Merge branch 'worktree-agent-afaf588d9c94630c4'
This commit is contained in:
commit
c8a42b2a26
|
|
@ -29,7 +29,7 @@ Measured feature by feature in [docs/parity.md](docs/parity.md) (4 October 2026)
|
|||
|
||||
- **≈ 99% of After Effects' features, weighted by importance**, counting partial features as half
|
||||
done (≈ 99.8% with per-feature fractions): 89 of 92 features done, 3 partial, none missing; every
|
||||
essential (P0) feature done; all 306 effects implemented in full, 243 of them on the GPU.
|
||||
essential (P0) feature done; all 306 effects implemented in full, 280 of them on the GPU.
|
||||
- **≈ 8.5 agent-hours of work remain**: about **2–3 hours of wall-clock time** with five Claude
|
||||
Opus 5.5 agents in parallel, ≈ 1 hour without learned models for Roto Brush and face tracking.
|
||||
- What is left: GPU kernels for the remaining visual effects, and learned-model quality for Roto
|
||||
|
|
|
|||
|
|
@ -716,13 +716,26 @@ fn sample_color(img: &Image, how: u32, clip: f64) -> ([f32; 3], f32) {
|
|||
|
||||
/// Color Link: colours the layer with a colour sampled from the Source Layer (its source, no
|
||||
/// masks or effects) or, with no source layer, from the layer itself.
|
||||
fn color_link(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
/// Color Link's sampled colour (RGB) and alpha: from the Source Layer, or from `img` (the
|
||||
/// layer's own pixels; `None` = the caller has not read them back) when no source is chosen.
|
||||
/// `None` when the own pixels are needed but not given.
|
||||
pub fn color_link_sample(ctx: &EffectCtx, img: Option<&Image>) -> Option<([f32; 3], f32)> {
|
||||
let how = ctx.params.e("sampleSource");
|
||||
let clip = ctx.params.f("clip") / 100.0;
|
||||
let (c, sa) = match ctx.layer_param("sourceLayer", false) {
|
||||
Some(match ctx.layer_param("sourceLayer", false) {
|
||||
Some(lp) => sample_color(&lp.buf.img, how, clip),
|
||||
None => sample_color(&b.img, how, clip),
|
||||
};
|
||||
None => sample_color(img?, how, clip),
|
||||
})
|
||||
}
|
||||
|
||||
/// Color Link's Blending Mode.
|
||||
pub fn color_link_mode(ctx: &EffectCtx) -> BlendMode {
|
||||
LINK_MODES[(ctx.params.e("blendingMode") as usize).min(LINK_MODES.len() - 1)].1
|
||||
}
|
||||
|
||||
fn color_link(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let how = ctx.params.e("sampleSource");
|
||||
let Some((c, sa)) = color_link_sample(ctx, Some(&b.img)) else { return b };
|
||||
let op = (ctx.params.f("opacity") / 100.0).clamp(0.0, 1.0) as f32;
|
||||
let stencil = ctx.params.b("stencilOriginalAlpha");
|
||||
if how >= 6 {
|
||||
|
|
@ -737,7 +750,7 @@ fn color_link(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
});
|
||||
return b;
|
||||
}
|
||||
let mode = LINK_MODES[(ctx.params.e("blendingMode") as usize).min(LINK_MODES.len() - 1)].1;
|
||||
let mode = color_link_mode(ctx);
|
||||
let src = [c[0] * op, c[1] * op, c[2] * op, op];
|
||||
b.img.data.par_iter_mut().for_each(|px| {
|
||||
let a0 = px[3];
|
||||
|
|
|
|||
|
|
@ -208,15 +208,18 @@ fn ellipse(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
|
||||
// ---------------------------------------------------------------- Lens Flare
|
||||
|
||||
struct Ghost {
|
||||
t: f64,
|
||||
r: f64,
|
||||
c: [f32; 3],
|
||||
k: f32,
|
||||
ring: bool,
|
||||
/// A Lens Flare ghost: position along the flare axis, radius (fraction of the diagonal), colour,
|
||||
/// strength, ring or disc.
|
||||
pub struct Ghost {
|
||||
pub t: f64,
|
||||
pub r: f64,
|
||||
pub c: [f32; 3],
|
||||
pub k: f32,
|
||||
pub ring: bool,
|
||||
}
|
||||
|
||||
fn ghosts(lens: u32) -> Vec<Ghost> {
|
||||
/// The ghosts of Lens Type `lens`.
|
||||
pub fn flare_ghosts(lens: u32) -> Vec<Ghost> {
|
||||
let g = |t, r, c: [f32; 3], k, ring| Ghost { t, r, c, k, ring };
|
||||
match lens {
|
||||
1 => vec![
|
||||
|
|
@ -246,7 +249,7 @@ fn lens_flare(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
let (x0, y0, w, h) = layer_rect(ctx, &b);
|
||||
let c = (x0 + w * 0.5, y0 + h * 0.5);
|
||||
let diag = (w * w + h * h).sqrt().max(1.0);
|
||||
let gs = ghosts(lens);
|
||||
let gs = flare_ghosts(lens);
|
||||
let (core_k, rays, halo_r) = match lens {
|
||||
1 => (0.035, 6.0, 0.18),
|
||||
2 => (0.02, 12.0, 0.3),
|
||||
|
|
@ -392,9 +395,9 @@ fn reflect(v: f64, lo: f64, hi: f64) -> f64 {
|
|||
|
||||
/// The source shape of a contour wave: signed distance (buffer px, negative inside) on the
|
||||
/// buffer grid, and the point of the shape that sits on the Producer Point.
|
||||
struct Contour {
|
||||
sdf: Plane,
|
||||
anchor: (f64, f64),
|
||||
pub struct Contour {
|
||||
pub sdf: Plane,
|
||||
pub anchor: (f64, f64),
|
||||
}
|
||||
|
||||
/// Image Contours: the region of the source layer whose Value Channel passes Value Threshold.
|
||||
|
|
@ -494,16 +497,27 @@ fn mask_contour(ctx: &EffectCtx, b: &Buf) -> Option<Contour> {
|
|||
}
|
||||
|
||||
/// One live wave.
|
||||
struct Wave {
|
||||
centre: (f64, f64),
|
||||
radius: f64,
|
||||
half_width: f64,
|
||||
rotation: f64,
|
||||
fade: f32,
|
||||
color: [f32; 4],
|
||||
opacity: f32,
|
||||
pub struct Wave {
|
||||
pub centre: (f64, f64),
|
||||
pub radius: f64,
|
||||
pub half_width: f64,
|
||||
pub rotation: f64,
|
||||
pub fade: f32,
|
||||
pub color: [f32; 4],
|
||||
pub opacity: f32,
|
||||
/// Polygon outline (unit shape rotated and scaled), or None for a circle / contour.
|
||||
outline: Option<Vec<(f64, f64)>>,
|
||||
pub outline: Option<Vec<(f64, f64)>>,
|
||||
}
|
||||
|
||||
/// Radio Waves, shared with the GPU compositor (effectcraft-gpu `fx_gen2`): the live waves,
|
||||
/// oldest first, and the contour they ripple from (Image Contours / Mask).
|
||||
pub struct RadioPlan {
|
||||
pub waves: Vec<Wave>,
|
||||
pub contour: Option<Contour>,
|
||||
/// Anti-aliasing width (px) from Render Quality.
|
||||
pub aa: f64,
|
||||
/// Stroke Profile.
|
||||
pub profile: u32,
|
||||
}
|
||||
|
||||
/// Radio Waves: waves are born at Frequency per second at the Producer Point and grow at
|
||||
|
|
@ -513,7 +527,9 @@ struct Wave {
|
|||
/// channel, Mask waves from a mask. With Parameters Are Set At Birth each wave keeps the values
|
||||
/// it was born with; Each Frame applies the current values to every wave. Stroke draws each wave
|
||||
/// with its Profile across the width.
|
||||
fn radio_waves(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
/// [`RadioPlan`] of Radio Waves on `b`'s geometry (`None` = no contour: the layer passes
|
||||
/// through).
|
||||
pub fn radio_plan(ctx: &EffectCtx, b: &Buf) -> Option<RadioPlan> {
|
||||
let cur = ctx.params;
|
||||
let t = ctx.time.max(0.0);
|
||||
let freq = cur.f("waveMotion/frequency").max(0.01);
|
||||
|
|
@ -524,14 +540,14 @@ fn radio_waves(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
let aa = 0.5 + 1.5 / quality;
|
||||
let profile = cur.e("waveStroke/profile");
|
||||
let reflection = cur.b("waveMotion/reflection");
|
||||
let (lx, ly, lw, lh) = layer_rect(ctx, &b);
|
||||
let (lx, ly, lw, lh) = layer_rect(ctx, b);
|
||||
let contour = match wave_type {
|
||||
1 => image_contour(ctx, &b),
|
||||
2 => mask_contour(ctx, &b),
|
||||
1 => image_contour(ctx, b),
|
||||
2 => mask_contour(ctx, b),
|
||||
_ => None,
|
||||
};
|
||||
if wave_type != 0 && contour.is_none() {
|
||||
return b;
|
||||
return None;
|
||||
}
|
||||
let k_hi = (t * freq).floor() as i64;
|
||||
let k_lo = (((t - life_now) * freq).floor() as i64 - 1).max(0).max(k_hi - 256);
|
||||
|
|
@ -598,6 +614,11 @@ fn radio_waves(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
})
|
||||
})
|
||||
.collect();
|
||||
Some(RadioPlan { waves, contour, aa, profile })
|
||||
}
|
||||
|
||||
fn radio_waves(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let Some(RadioPlan { waves, contour, aa, profile }) = radio_plan(ctx, &b) else { return b };
|
||||
map_xy(&mut b.img, |x, y, px| {
|
||||
let mut out = px;
|
||||
// Older waves first, so newer ones sit on top.
|
||||
|
|
@ -634,7 +655,9 @@ fn radio_waves(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
|
||||
// ---------------------------------------------------------------- Advanced Lightning
|
||||
|
||||
type Seg = ((f64, f64), (f64, f64), f32);
|
||||
/// A bolt segment of Advanced Lightning: start, end (buffer px) and intensity.
|
||||
pub type BoltSeg = ((f64, f64), (f64, f64), f32);
|
||||
type Seg = BoltSeg;
|
||||
|
||||
/// Advanced Lightning's Lightning Type options.
|
||||
const LIGHTNING_TYPES: [&str; 8] = ["Direction", "Strike", "Breaking", "Bouncy", "Omni", "Anywhere", "Vertical", "Two-Way Strike"];
|
||||
|
|
@ -715,7 +738,9 @@ fn bolt(cfg: &BoltCfg, a: (f64, f64), z: (f64, f64), depth: u32, inten: f32, mai
|
|||
}
|
||||
}
|
||||
|
||||
fn lightning_segments(ctx: &EffectCtx, b: &Buf) -> Vec<Seg> {
|
||||
/// Advanced Lightning's bolt on `b` (shared with the GPU compositor; `b.img` is read only for
|
||||
/// Alpha Obstacle).
|
||||
pub fn lightning_segments(ctx: &EffectCtx, b: &Buf) -> Vec<Seg> {
|
||||
let kind = ctx.params.e("lightningType");
|
||||
let decay_main = ctx.params.b("decayMainCore");
|
||||
let o = b.to_px(ctx.params.v2("origin"));
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ fn pct() -> ParamUi {
|
|||
|
||||
/// A round-capped line segment in buffer pixels with radius `r` and intensity `v`.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(crate) struct Seg {
|
||||
pub struct Seg {
|
||||
pub a: [f64; 2],
|
||||
pub b: [f64; 2],
|
||||
pub r: f64,
|
||||
|
|
@ -89,7 +89,7 @@ pub(crate) fn raster_segs(w: usize, h: usize, segs: &[Seg], hardness: f64) -> Pl
|
|||
}
|
||||
|
||||
/// Segments along a polyline (buffer px).
|
||||
pub(crate) fn poly_segs(pts: &[[f64; 2]], closed: bool, r: f64, v: f32, out: &mut Vec<Seg>) {
|
||||
pub fn poly_segs(pts: &[[f64; 2]], closed: bool, r: f64, v: f32, out: &mut Vec<Seg>) {
|
||||
let n = pts.len();
|
||||
if n == 1 {
|
||||
out.push(Seg { a: pts[0], b: pts[0], r, v });
|
||||
|
|
@ -147,6 +147,81 @@ pub(crate) fn paint(img: &mut Image, cov: &Plane, color: [f32; 4], opacity: f32,
|
|||
});
|
||||
}
|
||||
|
||||
/// Coverage of a plan-drawn generator: round-capped segments rasterised by [`raster_segs`]
|
||||
/// with `hardness`, or the union of convex polygons ([`raster_convex`]).
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Coverage {
|
||||
Segs { segs: Vec<Seg>, hardness: f64 },
|
||||
Convex(Vec<Vec<[f64; 2]>>),
|
||||
}
|
||||
|
||||
impl Coverage {
|
||||
fn raster(&self, w: usize, h: usize) -> Plane {
|
||||
match self {
|
||||
Coverage::Segs { segs, hardness } => raster_segs(w, h, segs, *hardness),
|
||||
Coverage::Convex(polys) => raster_convex(w, h, polys),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Stroke, Scribble, Vegas and Write-on, shared with the GPU compositor (effectcraft-gpu
|
||||
/// `fx_gen2`): the coverage and how it is painted. `style` 0–2 as [`paint`] (On Original
|
||||
/// Image, On Transparent, Reveal Original Image), 3 = under the layer (Vegas' Composite Under).
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PaintPlan {
|
||||
pub cov: Coverage,
|
||||
pub color: [f32; 4],
|
||||
pub opacity: f32,
|
||||
pub style: u32,
|
||||
}
|
||||
|
||||
/// Draw a [`PaintPlan`].
|
||||
fn draw_paint(b: &mut Buf, plan: &PaintPlan) {
|
||||
let cov = plan.cov.raster(b.img.width as usize, b.img.height as usize);
|
||||
if plan.style == 3 {
|
||||
let color = plan.color;
|
||||
b.img.data.par_iter_mut().zip(cov.data.par_iter()).for_each(|(px, &c)| {
|
||||
let a = c * color[3];
|
||||
*px = over([color[0] * a, color[1] * a, color[2] * a, a], *px);
|
||||
});
|
||||
} else {
|
||||
paint(&mut b.img, &cov, plan.color, plan.opacity, plan.style);
|
||||
}
|
||||
}
|
||||
|
||||
/// Generators drawn from a plan that need the layer's own pixels (`b.img`) to build it.
|
||||
pub fn plan_reads_pixels(id: &str, ctx: &EffectCtx) -> bool {
|
||||
match id {
|
||||
// Image Contours read the layer itself when no (resolvable) Input Layer is chosen.
|
||||
"ec.generate.vegas" => ctx.params.e("stroke") == 0,
|
||||
"ec.generate.paintbucket" | "ec.generate.eyedropperfill" => true,
|
||||
"ec.generate.advancedlightning" => ctx.params.f("alphaObstacle").clamp(-10.0, 10.0) != 0.0,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// [`PaintPlan`] of Stroke, Scribble, Vegas or Write-on on `b`'s geometry (`b.img` is read
|
||||
/// only when [`plan_reads_pixels`]). `None`: Scribble without a usable mask (the layer is
|
||||
/// cleared when it composites On Transparent, else kept).
|
||||
pub fn paint_plan(id: &str, ctx: &EffectCtx, b: &Buf) -> Option<PaintPlan> {
|
||||
match id {
|
||||
"ec.generate.stroke" => Some(stroke_plan(ctx, b)),
|
||||
"ec.generate.scribble" => scribble_plan(ctx, b),
|
||||
"ec.generate.vegas" => Some(vegas_plan(ctx, b)),
|
||||
"ec.generate.writeon" => Some(write_on_plan(ctx, b)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Scribble's Composite as a [`paint`] style.
|
||||
pub fn scribble_style(ctx: &EffectCtx) -> u32 {
|
||||
match ctx.params.e("composite") {
|
||||
0 => 0,
|
||||
2 => 2,
|
||||
_ => 1,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn over(dst: Px, src: Px) -> Px {
|
||||
let k = 1.0 - src[3];
|
||||
|
|
@ -602,21 +677,22 @@ pub(crate) fn raster_convex(w: usize, h: usize, polys: &[Vec<[f64; 2]>]) -> Plan
|
|||
}
|
||||
|
||||
fn scribble(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
match scribble_plan(ctx, &b) {
|
||||
Some(plan) => draw_paint(&mut b, &plan),
|
||||
None if scribble_style(ctx) == 1 => b.img = Image::new(b.img.width, b.img.height),
|
||||
None => {}
|
||||
}
|
||||
b
|
||||
}
|
||||
|
||||
fn scribble_plan(ctx: &EffectCtx, b: &Buf) -> Option<PaintPlan> {
|
||||
let pr = ctx.params;
|
||||
let which = pr.e("scribble");
|
||||
let polys: Vec<_> = if which == 0 { mask_px(ctx, &b, pr.f("mask").round().max(1.0) as usize).into_iter().collect() } else { all_masks_px(ctx, &b) };
|
||||
let polys: Vec<_> = if which == 0 { mask_px(ctx, b, pr.f("mask").round().max(1.0) as usize).into_iter().collect() } else { all_masks_px(ctx, b) };
|
||||
let polys: Vec<_> = polys.into_iter().filter(|p| p.0.len() >= 3).map(|(p, c, i)| (p, c, i && which == 2)).collect();
|
||||
let style = match pr.e("composite") {
|
||||
0 => 0,
|
||||
2 => 2,
|
||||
_ => 1,
|
||||
};
|
||||
let (w, h) = (b.img.width as usize, b.img.height as usize);
|
||||
let style = scribble_style(ctx);
|
||||
if polys.is_empty() {
|
||||
if style == 1 {
|
||||
b.img = Image::new(b.img.width, b.img.height);
|
||||
}
|
||||
return b;
|
||||
return None;
|
||||
}
|
||||
let sc = b.scale;
|
||||
let fill = pr.e("fillType");
|
||||
|
|
@ -742,24 +818,26 @@ fn scribble(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
for part in &parts {
|
||||
poly_segs(part, false, r, 1.0, &mut segs);
|
||||
}
|
||||
raster_segs(w, h, &segs, 0.9)
|
||||
Coverage::Segs { segs, hardness: 0.9 }
|
||||
} else {
|
||||
let polys = stroke_outline(&parts, r, cap, join, pr.f("edgeOptions/miterLimit").max(1.0));
|
||||
raster_convex(w, h, &polys)
|
||||
Coverage::Convex(stroke_outline(&parts, r, cap, join, pr.f("edgeOptions/miterLimit").max(1.0)))
|
||||
};
|
||||
let color = pr.color("color");
|
||||
paint(&mut b.img, &cov, color, (pr.f("opacity") / 100.0) as f32, style);
|
||||
b
|
||||
Some(PaintPlan { cov, color: pr.color("color"), opacity: (pr.f("opacity") / 100.0) as f32, style })
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Stroke
|
||||
|
||||
fn stroke(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let plan = stroke_plan(ctx, &b);
|
||||
draw_paint(&mut b, &plan);
|
||||
b
|
||||
}
|
||||
|
||||
fn stroke_plan(ctx: &EffectCtx, b: &Buf) -> PaintPlan {
|
||||
let pr = ctx.params;
|
||||
let all = pr.b("allMasks");
|
||||
let polys: Vec<_> = if all { all_masks_px(ctx, &b) } else { mask_px(ctx, &b, pr.f("path").round() as usize).into_iter().collect() };
|
||||
let polys: Vec<_> = if all { all_masks_px(ctx, b) } else { mask_px(ctx, b, pr.f("path").round() as usize).into_iter().collect() };
|
||||
let style = pr.e("paintStyle");
|
||||
let (w, h) = (b.img.width as usize, b.img.height as usize);
|
||||
let size = (pr.f("brushSize") * b.scale).max(0.1);
|
||||
let r = size * 0.5;
|
||||
let hard = pr.f("brushHardness") / 100.0;
|
||||
|
|
@ -795,9 +873,7 @@ fn stroke(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
}
|
||||
}
|
||||
}
|
||||
let cov = raster_segs(w, h, &segs, hard);
|
||||
paint(&mut b.img, &cov, pr.color("color"), (pr.f("opacity") / 100.0) as f32, style);
|
||||
b
|
||||
PaintPlan { cov: Coverage::Segs { segs, hardness: hard }, color: pr.color("color"), opacity: (pr.f("opacity") / 100.0) as f32, style }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Vegas
|
||||
|
|
@ -889,13 +965,19 @@ fn contours(pl: &Plane, thr: f32) -> Vec<(Vec<[f64; 2]>, bool)> {
|
|||
}
|
||||
|
||||
fn vegas(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let plan = vegas_plan(ctx, &b);
|
||||
draw_paint(&mut b, &plan);
|
||||
b
|
||||
}
|
||||
|
||||
fn vegas_plan(ctx: &EffectCtx, b: &Buf) -> PaintPlan {
|
||||
let pr = ctx.params;
|
||||
let (w, h) = (b.img.width as usize, b.img.height as usize);
|
||||
let sc = b.scale;
|
||||
let paths: Vec<(Vec<[f64; 2]>, bool)> = if pr.e("stroke") == 1 {
|
||||
mask_px(ctx, &b, pr.f("path").round() as usize).map(|(p, c, _)| vec![(p, c)]).unwrap_or_default()
|
||||
mask_px(ctx, b, pr.f("path").round() as usize).map(|(p, c, _)| vec![(p, c)]).unwrap_or_default()
|
||||
} else {
|
||||
let src = layer_or_self(ctx, &b, "inputLayer", true, pr.e("ifLayerSizesDiffer") == 1);
|
||||
let src = layer_or_self(ctx, b, "inputLayer", true, pr.e("ifLayerSizesDiffer") == 1);
|
||||
let ch = pr.e("channel");
|
||||
let inv = pr.b("invertInput");
|
||||
let mut pl = Plane::from_image(&src, |px| {
|
||||
|
|
@ -981,20 +1063,13 @@ fn vegas(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
}
|
||||
}
|
||||
}
|
||||
let cov = raster_segs(w, h, &segs, hard);
|
||||
let color = pr.color("color");
|
||||
match pr.e("blendMode") {
|
||||
1 => paint(&mut b.img, &cov, color, 1.0, 0),
|
||||
2 => {
|
||||
b.img.data.par_iter_mut().zip(cov.data.par_iter()).for_each(|(px, &c)| {
|
||||
let a = c * color[3];
|
||||
*px = over([color[0] * a, color[1] * a, color[2] * a, a], *px);
|
||||
});
|
||||
}
|
||||
3 => paint(&mut b.img, &cov, color, 1.0, 2),
|
||||
_ => paint(&mut b.img, &cov, color, 1.0, 1),
|
||||
}
|
||||
b
|
||||
let style = match pr.e("blendMode") {
|
||||
1 => 0,
|
||||
2 => 3,
|
||||
3 => 2,
|
||||
_ => 1,
|
||||
};
|
||||
PaintPlan { cov: Coverage::Segs { segs, hardness: hard }, color: pr.color("color"), opacity: 1.0, style }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Write-on / Glue Gun
|
||||
|
|
@ -1003,13 +1078,21 @@ fn vegas(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
/// (parameter histories are not passed in), so the stroke is drawn as the dabs laid along
|
||||
/// the current position — a single dab when the brush is static. Deterministic.
|
||||
fn write_on(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let plan = write_on_plan(ctx, &b);
|
||||
draw_paint(&mut b, &plan);
|
||||
b
|
||||
}
|
||||
|
||||
fn write_on_plan(ctx: &EffectCtx, b: &Buf) -> PaintPlan {
|
||||
let pr = ctx.params;
|
||||
let (w, h) = (b.img.width as usize, b.img.height as usize);
|
||||
let q = b.to_px(pr.v2("brushPosition"));
|
||||
let r = (pr.f("brushSize") * b.scale * 0.5).max(0.05);
|
||||
let cov = raster_segs(w, h, &[Seg { a: [q.0, q.1], b: [q.0, q.1], r, v: 1.0 }], pr.f("brushHardness") / 100.0);
|
||||
paint(&mut b.img, &cov, pr.color("color"), (pr.f("brushOpacity") / 100.0) as f32, pr.e("paintStyle"));
|
||||
b
|
||||
PaintPlan {
|
||||
cov: Coverage::Segs { segs: vec![Seg { a: [q.0, q.1], b: [q.0, q.1], r, v: 1.0 }], hardness: pr.f("brushHardness") / 100.0 },
|
||||
color: pr.color("color"),
|
||||
opacity: (pr.f("brushOpacity") / 100.0) as f32,
|
||||
style: pr.e("paintStyle"),
|
||||
}
|
||||
}
|
||||
|
||||
fn glue_gun(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
|
|
@ -1071,12 +1154,11 @@ fn glue_gun(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
|
||||
// ---------------------------------------------------------------- Paint Bucket
|
||||
|
||||
fn paint_bucket(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
/// Paint Bucket's filled region (1 inside) on `b`: the flood fill from the Fill Point over the
|
||||
/// layer's pixels (shared with the GPU compositor, which reads the layer back for it).
|
||||
pub fn paint_bucket_region(ctx: &EffectCtx, b: &Buf) -> Plane {
|
||||
let pr = ctx.params;
|
||||
let (w, h) = (b.img.width as usize, b.img.height as usize);
|
||||
if w == 0 || h == 0 {
|
||||
return b;
|
||||
}
|
||||
let fp = b.to_px(pr.v2("fillPoint"));
|
||||
let (sx, sy) = (fp.0.floor() as i64, fp.1.floor() as i64);
|
||||
let sel = pr.e("fillSelector");
|
||||
|
|
@ -1127,6 +1209,16 @@ fn paint_bucket(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
}
|
||||
}
|
||||
}
|
||||
region
|
||||
}
|
||||
|
||||
fn paint_bucket(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let pr = ctx.params;
|
||||
let (w, h) = (b.img.width as usize, b.img.height as usize);
|
||||
if w == 0 || h == 0 {
|
||||
return b;
|
||||
}
|
||||
let region = paint_bucket_region(ctx, &b);
|
||||
if pr.b("viewThreshold") {
|
||||
b.img.data.par_iter_mut().zip(region.data.par_iter()).for_each(|(px, &v)| *px = [v, v, v, 1.0]);
|
||||
return b;
|
||||
|
|
@ -1200,6 +1292,21 @@ fn paint_bucket(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
// ---------------------------------------------------------------- Eyedropper Fill
|
||||
|
||||
fn eyedropper_fill(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let pr = ctx.params;
|
||||
let fill = eyedropper_color(ctx, &b);
|
||||
let keep_alpha = pr.b("maintainOriginalAlpha");
|
||||
let blend = (pr.f("blendWithOriginal") / 100.0) as f32;
|
||||
b.img.data.par_iter_mut().for_each(|px| {
|
||||
let a = if keep_alpha { px[3] } else { fill[3] };
|
||||
let f = premul([fill[0], fill[1], fill[2]], a);
|
||||
*px = crate::util::lerp4(f, *px, blend);
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
/// Eyedropper Fill's sampled colour (straight RGB, alpha) from the layer's pixels (shared with
|
||||
/// the GPU compositor, which reads the layer back for it).
|
||||
pub fn eyedropper_color(ctx: &EffectCtx, b: &Buf) -> Px {
|
||||
let pr = ctx.params;
|
||||
let (w, h) = (b.img.width as i64, b.img.height as i64);
|
||||
let c = b.to_px(pr.v2("samplePoint"));
|
||||
|
|
@ -1250,7 +1357,7 @@ fn eyedropper_fill(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
}
|
||||
}
|
||||
}
|
||||
let fill: Px = if n > 0.0 {
|
||||
if n > 0.0 {
|
||||
if mode == 3 {
|
||||
let a = (acc[3] / n) as f32;
|
||||
let s = if a > 1e-6 { [(acc[0] / n) as f32 / a, (acc[1] / n) as f32 / a, (acc[2] / n) as f32 / a] } else { [0.0; 3] };
|
||||
|
|
@ -1260,15 +1367,7 @@ fn eyedropper_fill(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
}
|
||||
} else {
|
||||
[0.0, 0.0, 0.0, 0.0]
|
||||
};
|
||||
let keep_alpha = pr.b("maintainOriginalAlpha");
|
||||
let blend = (pr.f("blendWithOriginal") / 100.0) as f32;
|
||||
b.img.data.par_iter_mut().for_each(|px| {
|
||||
let a = if keep_alpha { px[3] } else { fill[3] };
|
||||
let f = premul([fill[0], fill[1], fill[2]], a);
|
||||
*px = crate::util::lerp4(f, *px, blend);
|
||||
});
|
||||
b
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Threads
|
||||
|
|
@ -1455,9 +1554,57 @@ fn draw_marks(b: &mut Buf, segs_core: &[Seg], thickness: f64, softness: f64, ins
|
|||
});
|
||||
}
|
||||
|
||||
fn audio_spectrum(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
/// Audio Spectrum / Audio Waveform, shared with the GPU compositor (effectcraft-gpu `fx_gen2`):
|
||||
/// the marks (the audio analysis runs on the CPU) and how [`draw_marks`] colours them.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MarksPlan {
|
||||
pub segs: Vec<Seg>,
|
||||
pub thickness: f64,
|
||||
pub softness: f64,
|
||||
pub inside: [f32; 4],
|
||||
pub outside: [f32; 4],
|
||||
pub composite: bool,
|
||||
}
|
||||
|
||||
/// [`MarksPlan`] of Audio Spectrum or Audio Waveform on `b`'s geometry (`None` = no audio:
|
||||
/// the layer passes through).
|
||||
pub fn marks_plan(id: &str, ctx: &EffectCtx, b: &Buf) -> Option<MarksPlan> {
|
||||
match id {
|
||||
"ec.generate.audiospectrum" => spectrum_plan(ctx, b),
|
||||
"ec.generate.audiowaveform" => waveform_plan(ctx, b),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn marks(ctx: &EffectCtx, segs: Vec<Seg>, thick: f64, soft: f64) -> MarksPlan {
|
||||
let pr = ctx.params;
|
||||
let Some(samples) = audio_window(ctx, pr.f("audioDuration"), pr.f("audioOffset"), 0) else { return b };
|
||||
MarksPlan {
|
||||
segs,
|
||||
thickness: thick,
|
||||
softness: soft * 2.0,
|
||||
inside: pr.color("insideColor"),
|
||||
outside: pr.color("outsideColor"),
|
||||
composite: pr.b("compositeOnOriginal"),
|
||||
}
|
||||
}
|
||||
|
||||
fn audio_spectrum(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
if let Some(m) = spectrum_plan(ctx, &b) {
|
||||
draw_marks(&mut b, &m.segs, m.thickness, m.softness, m.inside, m.outside, m.composite);
|
||||
}
|
||||
b
|
||||
}
|
||||
|
||||
fn audio_waveform(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
if let Some(m) = waveform_plan(ctx, &b) {
|
||||
draw_marks(&mut b, &m.segs, m.thickness, m.softness, m.inside, m.outside, m.composite);
|
||||
}
|
||||
b
|
||||
}
|
||||
|
||||
fn spectrum_plan(ctx: &EffectCtx, b: &Buf) -> Option<MarksPlan> {
|
||||
let pr = ctx.params;
|
||||
let samples = audio_window(ctx, pr.f("audioDuration"), pr.f("audioOffset"), 0)?;
|
||||
let n = samples.len().next_power_of_two().max(64);
|
||||
let mut re = vec![0.0; n];
|
||||
let mut im = vec![0.0; n];
|
||||
|
|
@ -1480,7 +1627,7 @@ fn audio_spectrum(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
let soft = pr.f("softness") / 100.0;
|
||||
let disp = pr.e("displayOptions");
|
||||
let side = pr.e("sideOptions");
|
||||
let base = Baseline::new(ctx, &b);
|
||||
let base = Baseline::new(ctx, b);
|
||||
let mut tops = Vec::with_capacity(bands);
|
||||
let mut segs = Vec::new();
|
||||
for i in 0..bands {
|
||||
|
|
@ -1522,19 +1669,18 @@ fn audio_spectrum(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
}
|
||||
}
|
||||
}
|
||||
draw_marks(&mut b, &segs, thick, soft * 2.0, pr.color("insideColor"), pr.color("outsideColor"), pr.b("compositeOnOriginal"));
|
||||
b
|
||||
Some(marks(ctx, segs, thick, soft))
|
||||
}
|
||||
|
||||
fn audio_waveform(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
fn waveform_plan(ctx: &EffectCtx, b: &Buf) -> Option<MarksPlan> {
|
||||
let pr = ctx.params;
|
||||
let Some(samples) = audio_window(ctx, pr.f("audioDuration"), pr.f("audioOffset"), pr.e("waveformOptions")) else { return b };
|
||||
let samples = audio_window(ctx, pr.f("audioDuration"), pr.f("audioOffset"), pr.e("waveformOptions"))?;
|
||||
let count = pr.f("displayedSamples").round().clamp(2.0, 4096.0) as usize;
|
||||
let max_h = pr.f("maximumHeight") * b.scale;
|
||||
let thick = (pr.f("thickness") * b.scale).max(0.1);
|
||||
let soft = pr.f("softness") / 100.0;
|
||||
let disp = pr.e("displayOptions");
|
||||
let base = Baseline::new(ctx, &b);
|
||||
let base = Baseline::new(ctx, b);
|
||||
let m = samples.len();
|
||||
let mut pts = Vec::with_capacity(count);
|
||||
let mut segs = Vec::new();
|
||||
|
|
@ -1567,8 +1713,7 @@ fn audio_waveform(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
} else if disp == 2 {
|
||||
segs.extend(pts.iter().map(|&q| Seg { a: q, b: q, r: thick * 0.5, v: 1.0 }));
|
||||
}
|
||||
draw_marks(&mut b, &segs, thick, soft * 2.0, pr.color("insideColor"), pr.color("outsideColor"), pr.b("compositeOnOriginal"));
|
||||
b
|
||||
Some(marks(ctx, segs, thick, soft))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- registry
|
||||
|
|
|
|||
|
|
@ -101,24 +101,40 @@ fn mask_sdf(ctx: &EffectCtx, b: &Buf, idx: &[u32]) -> Option<Plane> {
|
|||
Some(pl)
|
||||
}
|
||||
|
||||
fn inner_outer_key(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
/// Inner/Outer Key's geometry (shared with the GPU compositor, effectcraft-gpu `fx_pixel2`):
|
||||
/// the trimap of the selected masks and the Cleanup brush strokes, in buffer pixels.
|
||||
pub struct InnerOuterPlan {
|
||||
/// 1 = known foreground, 0 = known background, 0.5 = unknown.
|
||||
pub tri: Plane,
|
||||
/// Some pixel is unknown (edge colours are estimated and decontaminated).
|
||||
pub unknown: bool,
|
||||
/// Blur radius of the local foreground / background colour estimates.
|
||||
pub sigma: f64,
|
||||
/// Cleanup strokes in order: (matte target, coverage per pixel; negative = beyond the brush).
|
||||
pub strokes: Vec<(f32, Vec<f32>)>,
|
||||
/// Edge Thin, Edge Feather (buffer px), Edge Threshold, Invert Extraction, Blend with Original.
|
||||
pub thin: f64,
|
||||
pub feather: f64,
|
||||
pub threshold: f32,
|
||||
pub invert: bool,
|
||||
pub original: f32,
|
||||
}
|
||||
|
||||
/// [`InnerOuterPlan`] of an Inner/Outer Key instance on `b`'s geometry (`None` = no foreground
|
||||
/// mask: the layer passes through).
|
||||
pub fn inner_outer_plan(ctx: &EffectCtx, b: &Buf) -> Option<InnerOuterPlan> {
|
||||
// Foreground (Inside) plus Additional Foreground 1–10; likewise for the background.
|
||||
let fg_idx: Vec<u32> =
|
||||
std::iter::once(ctx.params.e("foreground")).chain((1..=10).map(|k| ctx.params.e(&format!("additionalForeground/foreground{k}")))).collect();
|
||||
let bg_idx: Vec<u32> =
|
||||
std::iter::once(ctx.params.e("background")).chain((1..=10).map(|k| ctx.params.e(&format!("additionalBackground/background{k}")))).collect();
|
||||
let radius = (ctx.params.f("singleMaskHighlightRadius") * b.scale) as f32;
|
||||
let thin = ctx.params.f("edgeThin") * b.scale;
|
||||
let feather = ctx.params.f("edgeFeather") * b.scale;
|
||||
let thr = (ctx.params.f("edgeThreshold") / 100.0) as f32;
|
||||
let invert = ctx.params.b("invertExtraction");
|
||||
let orig = (ctx.params.f("blendWithOriginal") / 100.0) as f32;
|
||||
let (w, h) = (b.img.width as usize, b.img.height as usize);
|
||||
// Trimap: 1 = known foreground, 0 = known background, 0.5 = unknown.
|
||||
let fg = mask_plane(ctx, &b, &fg_idx);
|
||||
let bg = mask_plane(ctx, &b, &bg_idx);
|
||||
let fg = mask_plane(ctx, b, &fg_idx);
|
||||
let bg = mask_plane(ctx, b, &bg_idx);
|
||||
let tri: Plane = match (fg, bg) {
|
||||
(None, _) => return b,
|
||||
(None, _) => return None,
|
||||
(Some(f), Some(bk)) => f.zip_map(&bk, |f, bk| {
|
||||
if f > 0.5 {
|
||||
1.0
|
||||
|
|
@ -132,7 +148,7 @@ fn inner_outer_key(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
if radius <= 0.0 {
|
||||
f
|
||||
} else {
|
||||
let sdf = mask_sdf(ctx, &b, &fg_idx).unwrap_or(f);
|
||||
let sdf = mask_sdf(ctx, b, &fg_idx).unwrap_or(f);
|
||||
sdf.map(|d| {
|
||||
if d > radius {
|
||||
1.0
|
||||
|
|
@ -146,11 +162,50 @@ fn inner_outer_key(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
}
|
||||
};
|
||||
let unknown = tri.data.par_iter().any(|&t| t > 0.25 && t < 0.75);
|
||||
let sigma = (radius.max(4.0) as f64).max(w.min(h) as f64 * 0.02);
|
||||
// Cleanup Foreground / Background: brush strokes along mask paths that paint the matte
|
||||
// towards opaque / transparent (Brush Radius, Brush Pressure).
|
||||
let mut strokes = Vec::new();
|
||||
for (side, target) in [("Foreground", 1.0f32), ("Background", 0.0)] {
|
||||
for k in 1..=10 {
|
||||
let g = format!("cleanup{side}/cleanup{side}{k}");
|
||||
let idx = ctx.params.e(&format!("{g}/path"));
|
||||
let Some(m) = (idx > 0).then(|| ctx.env.masks.get(idx as usize - 1)).flatten() else { continue };
|
||||
let rad = (ctx.params.f(&format!("{g}/brushRadius")) * b.scale).max(0.5);
|
||||
let pressure = (ctx.params.f(&format!("{g}/brushPressure")) / 100.0).clamp(0.0, 1.0) as f32;
|
||||
let pts: Vec<[f64; 2]> = m.points.iter().map(|q| [q[0] * b.scale + b.offset[0], q[1] * b.scale + b.offset[1]]).collect();
|
||||
let cov: Vec<f32> = (0..w * h)
|
||||
.into_par_iter()
|
||||
.map(|i| {
|
||||
let (x, y) = ((i % w) as f64 + 0.5, (i / w) as f64 + 0.5);
|
||||
let d = dist_to_poly(&pts, m.closed, x, y);
|
||||
if d < rad + 1.0 { ((rad - d) / rad.max(1.0) * 2.0).clamp(0.0, 1.0) as f32 * pressure } else { -1.0 }
|
||||
})
|
||||
.collect();
|
||||
strokes.push((target, cov));
|
||||
}
|
||||
}
|
||||
Some(InnerOuterPlan {
|
||||
tri,
|
||||
unknown,
|
||||
sigma,
|
||||
strokes,
|
||||
thin: ctx.params.f("edgeThin") * b.scale,
|
||||
feather: ctx.params.f("edgeFeather") * b.scale,
|
||||
threshold: (ctx.params.f("edgeThreshold") / 100.0) as f32,
|
||||
invert: ctx.params.b("invertExtraction"),
|
||||
original: (ctx.params.f("blendWithOriginal") / 100.0) as f32,
|
||||
})
|
||||
}
|
||||
|
||||
fn inner_outer_key(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let Some(plan) = inner_outer_plan(ctx, &b) else { return b };
|
||||
let InnerOuterPlan { tri, unknown, sigma, strokes, thin, feather, threshold: thr, invert, original: orig } = plan;
|
||||
let (w, h) = (b.img.width as usize, b.img.height as usize);
|
||||
let mut alpha = tri.clone();
|
||||
let mut colours: Option<Vec<[f32; 3]>> = None;
|
||||
if unknown {
|
||||
// Local foreground / background colour estimates: normalised blurs over known regions.
|
||||
let sigma = (radius.max(4.0) as f64).max(w.min(h) as f64 * 0.02);
|
||||
let known_f = tri.map(|t| if t > 0.75 { 1.0 } else { 0.0 });
|
||||
let known_b = tri.map(|t| if t < 0.25 { 1.0 } else { 0.0 });
|
||||
let straight: Vec<[f32; 3]> = b.img.data.par_iter().map(|&px| unpremul(px).0).collect();
|
||||
|
|
@ -190,25 +245,12 @@ fn inner_outer_key(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
.collect(),
|
||||
);
|
||||
}
|
||||
// Cleanup Foreground / Background: brush strokes along mask paths that paint the matte
|
||||
// towards opaque / transparent (Brush Radius, Brush Pressure).
|
||||
for (side, target) in [("Foreground", 1.0f32), ("Background", 0.0)] {
|
||||
for k in 1..=10 {
|
||||
let g = format!("cleanup{side}/cleanup{side}{k}");
|
||||
let idx = ctx.params.e(&format!("{g}/path"));
|
||||
let Some(m) = (idx > 0).then(|| ctx.env.masks.get(idx as usize - 1)).flatten() else { continue };
|
||||
let rad = (ctx.params.f(&format!("{g}/brushRadius")) * b.scale).max(0.5);
|
||||
let pressure = (ctx.params.f(&format!("{g}/brushPressure")) / 100.0).clamp(0.0, 1.0) as f32;
|
||||
let pts: Vec<[f64; 2]> = m.points.iter().map(|q| [q[0] * b.scale + b.offset[0], q[1] * b.scale + b.offset[1]]).collect();
|
||||
alpha.data.par_iter_mut().enumerate().for_each(|(i, a)| {
|
||||
let (x, y) = ((i % w) as f64 + 0.5, (i / w) as f64 + 0.5);
|
||||
let d = dist_to_poly(&pts, m.closed, x, y);
|
||||
if d < rad + 1.0 {
|
||||
let cov = ((rad - d) / rad.max(1.0) * 2.0).clamp(0.0, 1.0) as f32 * pressure;
|
||||
*a += (target - *a) * cov;
|
||||
}
|
||||
});
|
||||
}
|
||||
for (target, cov) in &strokes {
|
||||
alpha.data.par_iter_mut().zip(cov.par_iter()).for_each(|(a, &c)| {
|
||||
if c >= 0.0 {
|
||||
*a += (target - *a) * c;
|
||||
}
|
||||
});
|
||||
}
|
||||
if thin.abs() > 0.01 {
|
||||
alpha = morph_frac(&alpha, thin.abs(), thin < 0.0);
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ pub use color_fx::{
|
|||
HUESAT_CHANNELS, HUESAT_RANGES, LEVELS_CHANNELS, PHOTO_FILTER_CUSTOM, PHOTO_FILTERS, exposure_settings, fill_uses_masks, huesat_ranges_identity,
|
||||
levels_channel_ids, levels_channel_settings, levels_channels_identity, levels_clip,
|
||||
};
|
||||
pub use color2::{Curve, auto_correct_settings, equalize_tables, luma_clip_points, shadow_highlight_amounts};
|
||||
pub use color2::{Curve, auto_correct_settings, color_link_mode, color_link_sample, equalize_tables, luma_clip_points, shadow_highlight_amounts};
|
||||
pub use color3::OffsetCurve;
|
||||
pub use distort::transform_shutter;
|
||||
pub use distort2::{MAGNIFY_MODES, bezier_grid, parse_mesh};
|
||||
|
|
@ -89,11 +89,18 @@ pub use stylize2::strobe_on;
|
|||
pub use blur2::{camera_lens_plain, camera_lens_spans};
|
||||
pub use generate::gen_mode;
|
||||
pub use generate2::pattern_kind as cell_pattern_kind;
|
||||
pub use generate2::{BoltSeg, Contour, Ghost, RadioPlan, Wave, flare_ghosts, lightning_segments, radio_plan};
|
||||
pub use generate3::{
|
||||
Coverage, MarksPlan, PaintPlan, Seg, eyedropper_color, marks_plan, paint_bucket_region, paint_plan, plan_reads_pixels, poly_segs, scribble_style,
|
||||
};
|
||||
pub use noise::GrainLook;
|
||||
pub use textfx::{BoltPlan, TextLook, TextPass, bolt_plan, text_passes};
|
||||
pub use transition::place_layer;
|
||||
// effectcraft-gpu fx_noise.
|
||||
pub use keying2::{InnerOuterPlan, inner_outer_plan};
|
||||
pub use noise::remove_grain_level;
|
||||
pub use noise3::{FractalGpu, fractal_gpu};
|
||||
pub use stylize3::{BumpLight, bump_layer_id};
|
||||
pub use textfx::{TextLayer, text_layer};
|
||||
pub use time_fx::{PixelMotion, TimewarpPlan, TwStep, posterized_time, time_frames, timewarp_crop, timewarp_plan};
|
||||
// effectcraft-gpu fx_tone.
|
||||
|
|
@ -1028,6 +1035,45 @@ pub const GPU_EFFECTS: &[&str] = &[
|
|||
"ec.time.ccwidetime",
|
||||
"ec.time.pixelmotionblur",
|
||||
"ec.time.timewarp",
|
||||
// effectcraft-gpu fx_pixel2 (pixel ports, part C)
|
||||
"ec.blur.cccross",
|
||||
"ec.blur.ccradial",
|
||||
"ec.blur.ccvector",
|
||||
"ec.blur.reduceflicker",
|
||||
"ec.channel.cccomposite",
|
||||
"ec.color.colorlink",
|
||||
"ec.utility.cineon",
|
||||
"ec.utility.hdrcompander",
|
||||
"ec.utility.hdrcompression",
|
||||
"ec.utility.growbounds",
|
||||
"ec.utility.ccoverbrights",
|
||||
"ec.stylize.ccblockload",
|
||||
"ec.stylize.ccburnfilm",
|
||||
"ec.stylize.ccglass",
|
||||
"ec.stylize.cchextile",
|
||||
"ec.stylize.ccmrsmoothie",
|
||||
"ec.stylize.ccplastic",
|
||||
"ec.key.innerouter",
|
||||
"ec.key.ccsimplewireremoval",
|
||||
"ec.obsolete.basic3d",
|
||||
// effectcraft-gpu fx_gen2 (generators, part C: CPU plans rasterised on the GPU)
|
||||
"ec.obsolete.lightning",
|
||||
"ec.generate.advancedlightning",
|
||||
"ec.generate.beam",
|
||||
"ec.generate.lensflare",
|
||||
"ec.generate.radiowaves",
|
||||
"ec.generate.vegas",
|
||||
"ec.generate.stroke",
|
||||
"ec.generate.scribble",
|
||||
"ec.generate.writeon",
|
||||
"ec.generate.paintbucket",
|
||||
"ec.generate.eyedropperfill",
|
||||
"ec.generate.ccgluegun",
|
||||
"ec.generate.ccthreads",
|
||||
"ec.generate.audiospectrum",
|
||||
"ec.generate.audiowaveform",
|
||||
"ec.obsolete.basictext",
|
||||
"ec.obsolete.pathtext",
|
||||
];
|
||||
|
||||
/// Effects whose output depends on [`EffectCtx::time`] directly (not only through animated
|
||||
|
|
|
|||
|
|
@ -180,9 +180,14 @@ fn burn_film(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
|
||||
const BUMP_PROPS: [&str; 6] = ["Red", "Green", "Blue", "Alpha", "Luminance", "Lightness"];
|
||||
|
||||
/// The bump-map layer parameter: CC Glass's Bump Map, CC Plastic's Bump Layer.
|
||||
pub fn bump_layer_id(ctx: &EffectCtx) -> &'static str {
|
||||
if ctx.params.get("bumpLayer").is_some() { "bumpLayer" } else { "bumpMap" }
|
||||
}
|
||||
|
||||
/// Height field (0..1) from the bump-map layer (or the layer itself), softened.
|
||||
fn height_field(ctx: &EffectCtx, b: &Buf) -> Plane {
|
||||
let src = layer_or_self(ctx, b, "bumpMap", true, false);
|
||||
let src = layer_or_self(ctx, b, bump_layer_id(ctx), true, false);
|
||||
let prop = ctx.params.e("property");
|
||||
let mut pl = Plane::from_image(&src, |p| {
|
||||
let (c, a) = unpremul(p);
|
||||
|
|
@ -203,30 +208,31 @@ fn height_field(ctx: &EffectCtx, b: &Buf) -> Plane {
|
|||
pl
|
||||
}
|
||||
|
||||
struct Light {
|
||||
intensity: f32,
|
||||
color: [f32; 3],
|
||||
point: bool,
|
||||
pos: (f64, f64),
|
||||
/// The effect light and surface of CC Glass / CC Plastic (shared with the GPU compositor).
|
||||
pub struct BumpLight {
|
||||
pub intensity: f32,
|
||||
pub color: [f32; 3],
|
||||
pub point: bool,
|
||||
pub pos: (f64, f64),
|
||||
/// Distant light direction (towards the light).
|
||||
dir: [f32; 3],
|
||||
height: f32,
|
||||
ambient: f32,
|
||||
diffuse: f32,
|
||||
specular: f32,
|
||||
shininess: f32,
|
||||
metal: f32,
|
||||
pub dir: [f32; 3],
|
||||
pub height: f32,
|
||||
pub ambient: f32,
|
||||
pub diffuse: f32,
|
||||
pub specular: f32,
|
||||
pub shininess: f32,
|
||||
pub metal: f32,
|
||||
}
|
||||
|
||||
impl Light {
|
||||
fn from(ctx: &EffectCtx, b: &Buf) -> Light {
|
||||
impl BumpLight {
|
||||
pub fn from(ctx: &EffectCtx, b: &Buf) -> BumpLight {
|
||||
let pr = &ctx.params;
|
||||
let elev = (pr.f("lightHeight").clamp(-100.0, 100.0) / 100.0 * 90.0).to_radians();
|
||||
let az = pr.f("lightDirection").to_radians();
|
||||
let dir = [(az.sin() * elev.cos()) as f32, (-az.cos() * elev.cos()) as f32, elev.sin() as f32];
|
||||
let c = pr.color("lightColor");
|
||||
let rough = pr.f("roughness").clamp(0.001, 1.0) as f32;
|
||||
Light {
|
||||
BumpLight {
|
||||
intensity: (pr.f("lightIntensity") / 100.0) as f32,
|
||||
color: [c[0], c[1], c[2]],
|
||||
point: pr.e("lightType") == 1,
|
||||
|
|
@ -288,7 +294,7 @@ fn glass(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
let height = ctx.params.f("height") as f32 / 100.0;
|
||||
let k = height * 25.0;
|
||||
let disp = ctx.params.f("displacement") * b.scale * 0.25;
|
||||
let light = Light::from(ctx, &b);
|
||||
let light = BumpLight::from(ctx, &b);
|
||||
let src = b.img.clone();
|
||||
b.img.rows_mut().for_each(|(y, row)| {
|
||||
for (x, px) in row.iter_mut().enumerate() {
|
||||
|
|
@ -321,7 +327,7 @@ fn plastic(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
hf = hf.map(|v| v.clamp(lo.min(hi), hi.max(lo)));
|
||||
}
|
||||
let k = ctx.params.f("height") as f32 / 100.0 * 25.0;
|
||||
let light = Light::from(ctx, &b);
|
||||
let light = BumpLight::from(ctx, &b);
|
||||
b.img.rows_mut().for_each(|(y, row)| {
|
||||
for (x, px) in row.iter_mut().enumerate() {
|
||||
if px[3] <= 0.0 {
|
||||
|
|
|
|||
|
|
@ -173,13 +173,35 @@ fn layout_line(text: &str, origin: [f64; 2], size: f64, tracking: f64, proportio
|
|||
}
|
||||
|
||||
/// How text paints: fill/stroke display option, colours, and compositing.
|
||||
struct TextLook {
|
||||
display: u32,
|
||||
fill: [f32; 4],
|
||||
stroke: [f32; 4],
|
||||
stroke_w: f64,
|
||||
on_original: bool,
|
||||
opacity: f32,
|
||||
/// How text is drawn: Display Options (0 fill only, 1 stroke only, 2 fill over stroke, 3 stroke
|
||||
/// over fill), colours, stroke width (buffer px), on the layer or on transparency, opacity.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct TextLook {
|
||||
pub display: u32,
|
||||
pub fill: [f32; 4],
|
||||
pub stroke: [f32; 4],
|
||||
pub stroke_w: f64,
|
||||
pub on_original: bool,
|
||||
pub opacity: f32,
|
||||
}
|
||||
|
||||
/// One drawing pass of Basic Text / Path Text, shared with the GPU compositor (effectcraft-gpu
|
||||
/// `fx_gen2`): glyph stroke polylines (buffer px) of radius `r`, drawn with `look` over the
|
||||
/// result of the previous pass.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TextPass {
|
||||
pub polys: Vec<Vec<[f64; 2]>>,
|
||||
pub r: f64,
|
||||
pub look: TextLook,
|
||||
}
|
||||
|
||||
/// The [`TextPass`]es of Basic Text or Path Text on `b`'s geometry.
|
||||
pub fn text_passes(id: &str, ctx: &EffectCtx, b: &Buf) -> Option<Vec<TextPass>> {
|
||||
match id {
|
||||
"ec.obsolete.basictext" => Some(vec![basic_text_pass(ctx, b)]),
|
||||
"ec.obsolete.pathtext" => Some(path_text_passes(ctx, b)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn over(dst: Px, src: Px) -> Px {
|
||||
|
|
@ -508,6 +530,12 @@ fn timecode_fx(ctx: &EffectCtx, b: Buf) -> Buf {
|
|||
// ---------------------------------------------------------------- Basic Text
|
||||
|
||||
fn basic_text(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let pass = basic_text_pass(ctx, &b);
|
||||
draw_text(&mut b, &pass.polys, pass.r, &pass.look);
|
||||
b
|
||||
}
|
||||
|
||||
fn basic_text_pass(ctx: &EffectCtx, b: &Buf) -> TextPass {
|
||||
let pr = ctx.params;
|
||||
let size = (pr.f("size") * b.scale).max(0.5);
|
||||
let tracking = pr.f("tracking") * b.scale;
|
||||
|
|
@ -527,21 +555,27 @@ fn basic_text(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
let y = pos.1 + unit(size) * 3.0 + (i as f64 - (n - 1.0) * 0.5) * lead;
|
||||
layout_line(l, [x, y], size, tracking, true, &mut polys);
|
||||
}
|
||||
let look = look_from(ctx, &b, "compositeOnOriginal");
|
||||
draw_text(&mut b, &polys, weight(size), &look);
|
||||
b
|
||||
let look = look_from(ctx, b, "compositeOnOriginal");
|
||||
TextPass { polys, r: weight(size), look }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Path Text
|
||||
|
||||
fn path_text(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
for pass in path_text_passes(ctx, &b) {
|
||||
draw_text(&mut b, &pass.polys, pass.r, &pass.look);
|
||||
}
|
||||
b
|
||||
}
|
||||
|
||||
fn path_text_passes(ctx: &EffectCtx, b: &Buf) -> Vec<TextPass> {
|
||||
let pr = ctx.params;
|
||||
let g = |group: &str, id: &str| grouped(pr, group, id).cloned().unwrap_or(Value::Scalar(0.0));
|
||||
let px = |id: &str| {
|
||||
let q = b.to_px(g("pathOptions/controlPoints", id).as_vec2());
|
||||
[q.0, q.1]
|
||||
};
|
||||
let (mut path, mut closed) = match mask_px(ctx, &b, g("pathOptions", "customPath").as_f64().round() as usize) {
|
||||
let (mut path, mut closed) = match mask_px(ctx, b, g("pathOptions", "customPath").as_f64().round() as usize) {
|
||||
Some((p, c, _)) => (p, c),
|
||||
None => match g("pathOptions", "shapeType").as_enum() {
|
||||
1 | 2 => {
|
||||
|
|
@ -675,12 +709,13 @@ fn path_text(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
}
|
||||
}
|
||||
}
|
||||
let mut look = look_from(ctx, &b, "compositeOnOriginal");
|
||||
let mut look = look_from(ctx, b, "compositeOnOriginal");
|
||||
let base_op = look.opacity;
|
||||
let mut first = true;
|
||||
for (alpha, polys) in &polys_by_alpha {
|
||||
look.opacity = base_op * alpha;
|
||||
draw_text(&mut b, polys, weight(size), &look);
|
||||
let mut passes = Vec::new();
|
||||
for (alpha, polys) in polys_by_alpha.iter_mut() {
|
||||
look.opacity = base_op * *alpha;
|
||||
passes.push(TextPass { polys: std::mem::take(polys), r: weight(size), look });
|
||||
if first {
|
||||
// Later passes go over the first one.
|
||||
look.on_original = true;
|
||||
|
|
@ -688,9 +723,9 @@ fn path_text(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
}
|
||||
}
|
||||
if polys_by_alpha.is_empty() {
|
||||
draw_text(&mut b, &[], weight(size), &look);
|
||||
passes.push(TextPass { polys: vec![], r: weight(size), look });
|
||||
}
|
||||
b
|
||||
passes
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Lightning (obsolete)
|
||||
|
|
@ -750,7 +785,53 @@ impl Bolt<'_> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Lightning, shared with the GPU compositor (effectcraft-gpu `fx_gen2`): the bolt segments
|
||||
/// (outer radius per segment) and how they are drawn.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct BoltPlan {
|
||||
pub segs: Vec<Seg>,
|
||||
/// Width (buffer px): the glow's blur.
|
||||
pub width: f64,
|
||||
/// Core Width (fraction of each segment's radius).
|
||||
pub core: f64,
|
||||
pub outside: [f32; 4],
|
||||
pub inside: [f32; 4],
|
||||
/// Blending Mode: 0 normal, 1 add, 2 screen.
|
||||
pub mode: u32,
|
||||
}
|
||||
|
||||
fn lightning(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let plan = bolt_plan(ctx, &b);
|
||||
let (segs, width, core) = (&plan.segs, plan.width, plan.core);
|
||||
let (wd, ht) = (b.img.width as usize, b.img.height as usize);
|
||||
let outer = raster_segs(wd, ht, segs, 0.0);
|
||||
let core_segs: Vec<Seg> = segs.iter().map(|g| Seg { r: (g.r * core).max(0.3), ..*g }).collect();
|
||||
let inner = raster_segs(wd, ht, &core_segs, 0.6);
|
||||
let glow = gauss_plane(&outer, width * 0.5, width * 0.5);
|
||||
let (oc, ic, mode) = (plan.outside, plan.inside, plan.mode);
|
||||
b.img.data.par_iter_mut().enumerate().for_each(|(i, px)| {
|
||||
let o = outer.data[i].max(glow.data[i] * 0.8);
|
||||
let c = inner.data[i];
|
||||
let a = o.max(c).min(1.0);
|
||||
if a <= 0.0 {
|
||||
return;
|
||||
}
|
||||
let t = if a > 0.0 { c / a } else { 0.0 };
|
||||
let colr = [oc[0] + (ic[0] - oc[0]) * t, oc[1] + (ic[1] - oc[1]) * t, oc[2] + (ic[2] - oc[2]) * t];
|
||||
*px = match mode {
|
||||
1 => [px[0] + colr[0] * a, px[1] + colr[1] * a, px[2] + colr[2] * a, (px[3] + a * (1.0 - px[3])).min(1.0)],
|
||||
2 => {
|
||||
let sc = |d: f32, s: f32| d + s - d * s;
|
||||
[sc(px[0], colr[0] * a), sc(px[1], colr[1] * a), sc(px[2], colr[2] * a), sc(px[3], a)]
|
||||
}
|
||||
_ => over(*px, [colr[0] * a, colr[1] * a, colr[2] * a, a]),
|
||||
};
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
/// [`BoltPlan`] of Lightning on `b`'s geometry.
|
||||
pub fn bolt_plan(ctx: &EffectCtx, b: &Buf) -> BoltPlan {
|
||||
let pr = ctx.params;
|
||||
let s = b.to_px(pr.v2("startPoint"));
|
||||
let e = b.to_px(pr.v2("endPoint"));
|
||||
|
|
@ -816,33 +897,7 @@ fn lightning(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
bolt.out.push(Seg { a: wpair[0], b: wpair[1], r: width * 0.5 * w * v, v: 1.0 });
|
||||
}
|
||||
}
|
||||
let (wd, ht) = (b.img.width as usize, b.img.height as usize);
|
||||
let outer = raster_segs(wd, ht, &segs, 0.0);
|
||||
let core_segs: Vec<Seg> = segs.iter().map(|g| Seg { r: (g.r * core).max(0.3), ..*g }).collect();
|
||||
let inner = raster_segs(wd, ht, &core_segs, 0.6);
|
||||
let glow = gauss_plane(&outer, width * 0.5, width * 0.5);
|
||||
let oc = pr.color("outsideColor");
|
||||
let ic = pr.color("insideColor");
|
||||
let mode = pr.e("blendingMode");
|
||||
b.img.data.par_iter_mut().enumerate().for_each(|(i, px)| {
|
||||
let o = outer.data[i].max(glow.data[i] * 0.8);
|
||||
let c = inner.data[i];
|
||||
let a = o.max(c).min(1.0);
|
||||
if a <= 0.0 {
|
||||
return;
|
||||
}
|
||||
let t = if a > 0.0 { c / a } else { 0.0 };
|
||||
let colr = [oc[0] + (ic[0] - oc[0]) * t, oc[1] + (ic[1] - oc[1]) * t, oc[2] + (ic[2] - oc[2]) * t];
|
||||
*px = match mode {
|
||||
1 => [px[0] + colr[0] * a, px[1] + colr[1] * a, px[2] + colr[2] * a, (px[3] + a * (1.0 - px[3])).min(1.0)],
|
||||
2 => {
|
||||
let sc = |d: f32, s: f32| d + s - d * s;
|
||||
[sc(px[0], colr[0] * a), sc(px[1], colr[1] * a), sc(px[2], colr[2] * a), sc(px[3], a)]
|
||||
}
|
||||
_ => over(*px, [colr[0] * a, colr[1] * a, colr[2] * a, a]),
|
||||
};
|
||||
});
|
||||
b
|
||||
BoltPlan { segs, width, core, outside: pr.color("outsideColor"), inside: pr.color("insideColor"), mode: pr.e("blendingMode") }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- registry
|
||||
|
|
|
|||
|
|
@ -0,0 +1,637 @@
|
|||
//! Golden hashes of the CPU effects that draw through a shared per-frame plan (M13.28).
|
||||
//!
|
||||
//! The GPU compositor rasterises these effects from geometry the CPU computes (bolts, strokes,
|
||||
//! glyphs, audio marks, wave lists, flood-fill regions, sampled colours); the CPU effects were
|
||||
//! refactored to build the same plans, and must render exactly what they did before. Each
|
||||
//! effect renders on a generated layer with masks, another layer and an audio track, at default
|
||||
//! and non-default parameters and two layer times, in an 8 bpc pipeline (input and output
|
||||
//! clamped and quantised) and a 32 bpc one, at full and at half resolution. Each frame's pixels
|
||||
//! are hashed (FNV-1a over the `f32` bits) and compared with the pinned hashes below.
|
||||
//!
|
||||
//! The hashes are exact `f32` bit patterns, so they depend on the platform's libm; they are
|
||||
//! pinned for aarch64 macOS (where the gates run). Elsewhere the test only checks that every
|
||||
//! case renders deterministically.
|
||||
//!
|
||||
//! `GEN_GOLDEN_PRINT=1` prints the table (to re-pin after an intended change).
|
||||
|
||||
use effectcraft_effects::{Buf, EffectCtx, EffectEnv, EffectHost, LayerPixels, MaskShape, Params, apply, default_value, find};
|
||||
use effectcraft_keyframe::Value;
|
||||
use effectcraft_raster::Image;
|
||||
|
||||
const W: u32 = 64;
|
||||
const H: u32 = 48;
|
||||
/// Layer id served by [`Host`]: a radial luminance map (also the audio layer).
|
||||
const MAP: u64 = 8;
|
||||
|
||||
/// The effect's own layer: a colour ramp with a clear hole and a half-transparent band.
|
||||
fn layer(hdr: bool) -> Image {
|
||||
let mut img = Image::new(W, H);
|
||||
for y in 0..H {
|
||||
for x in 0..W {
|
||||
let (u, v) = (x as f32 / W as f32, y as f32 / H as f32);
|
||||
let (dx, dy) = (x as f32 - 40.0, y as f32 - 20.0);
|
||||
let a = if dx * dx + dy * dy < 64.0 {
|
||||
0.0
|
||||
} else if (30..36).contains(&y) {
|
||||
0.45
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
let gain = if hdr && x < 20 { 1.6 } else { 1.0 };
|
||||
let c = [u * gain, v * gain, (0.3 + 0.4 * (u * 9.0).sin().abs()) * gain];
|
||||
img.set(x, y, [c[0] * a, c[1] * a, c[2] * a, a]);
|
||||
}
|
||||
}
|
||||
img
|
||||
}
|
||||
|
||||
fn map() -> Image {
|
||||
let mut img = Image::new(W, H);
|
||||
for y in 0..H {
|
||||
for x in 0..W {
|
||||
let d = ((x as f32 - 30.0).powi(2) + (y as f32 - 22.0).powi(2)).sqrt() / 30.0;
|
||||
let l = (1.0 - d).clamp(0.0, 1.0);
|
||||
img.set(x, y, [l, l * 0.8, l * 0.5, 1.0]);
|
||||
}
|
||||
}
|
||||
img
|
||||
}
|
||||
|
||||
struct Host;
|
||||
impl EffectHost for Host {
|
||||
fn layer(&self, _: u64, _: bool) -> Option<LayerPixels> {
|
||||
Some(LayerPixels { buf: Buf { img: map(), offset: [0.0; 2], scale: 1.0 }, size: [W as f64, H as f64] })
|
||||
}
|
||||
fn audio(&self, _: u64, start: f64, frames: usize, rate: u32) -> Option<Vec<f32>> {
|
||||
// Two tones plus a chirp, stereo interleaved.
|
||||
Some(
|
||||
(0..frames)
|
||||
.flat_map(|i| {
|
||||
let t = start + i as f64 / rate as f64;
|
||||
let l = 0.5 * (t * 440.0 * std::f64::consts::TAU).sin() + 0.2 * (t * 1300.0 * std::f64::consts::TAU).sin();
|
||||
let r = 0.4 * (t * (200.0 + 300.0 * t) * std::f64::consts::TAU).sin();
|
||||
[l as f32, r as f32]
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// The layer's masks (layer pixels): a closed pentagon, a closed quad and an open curve.
|
||||
fn masks() -> Vec<MaskShape> {
|
||||
let pent = (0..5)
|
||||
.map(|i| {
|
||||
let a = i as f64 * std::f64::consts::TAU / 5.0 - 1.3;
|
||||
[30.0 + a.cos() * 17.0, 24.0 + a.sin() * 15.0]
|
||||
})
|
||||
.collect();
|
||||
vec![
|
||||
MaskShape { name: "Mask 1".into(), points: pent, closed: true, inverted: false },
|
||||
MaskShape { name: "Mask 2".into(), points: vec![[6.3, 5.1], [58.2, 7.7], [55.4, 43.6], [9.1, 40.2]], closed: true, inverted: false },
|
||||
MaskShape { name: "Mask 3".into(), points: vec![[4.2, 30.3], [18.7, 12.4], [33.1, 28.9], [47.6, 9.8], [60.2, 25.5]], closed: false, inverted: false },
|
||||
]
|
||||
}
|
||||
|
||||
fn n(v: f64) -> Value {
|
||||
Value::Scalar(v)
|
||||
}
|
||||
fn e(v: u32) -> Value {
|
||||
Value::Enum(v)
|
||||
}
|
||||
fn b(v: bool) -> Value {
|
||||
Value::Bool(v)
|
||||
}
|
||||
fn pt(x: f64, y: f64) -> Value {
|
||||
Value::Vec2([x, y])
|
||||
}
|
||||
fn c(r: f64, g: f64, bl: f64) -> Value {
|
||||
Value::Color([r, g, bl, 1.0])
|
||||
}
|
||||
|
||||
type Set = Vec<(&'static str, Value)>;
|
||||
|
||||
/// (effect id, parameter sets) — the first set of each is the defaults.
|
||||
fn cases() -> Vec<(&'static str, Vec<Set>)> {
|
||||
vec![
|
||||
(
|
||||
"ec.obsolete.lightning",
|
||||
vec![
|
||||
vec![],
|
||||
vec![("branching", n(0.9)), ("rebranching", n(0.6)), ("detailLevel", n(4.0)), ("blendingMode", e(1)), ("pullForce", n(40.0))],
|
||||
vec![("blendingMode", e(2)), ("rerunAtEachFrame", b(true)), ("fixedEndpoint", b(true)), ("coreWidth", n(0.8))],
|
||||
],
|
||||
),
|
||||
(
|
||||
"ec.generate.advancedlightning",
|
||||
vec![
|
||||
vec![],
|
||||
vec![("lightningType", e(4)), ("forking", n(60.0)), ("decayMainCore", b(true)), ("expertSettings/coreDrain", n(40.0))],
|
||||
vec![("alphaObstacle", n(6.0)), ("compositeOnOriginal", b(false)), ("expertSettings/fractalType", e(2)), ("glowSettings/glowRadius", n(10.0))],
|
||||
vec![("lightningType", e(7)), ("alphaObstacle", n(-4.0)), ("expertSettings/forkVariation", n(50.0))],
|
||||
],
|
||||
),
|
||||
(
|
||||
"ec.generate.radiowaves",
|
||||
vec![
|
||||
vec![],
|
||||
vec![
|
||||
("polygon/sides", n(5.0)),
|
||||
("polygon/star", b(true)),
|
||||
("waveMotion/spin", n(30.0)),
|
||||
("waveStroke/profile", e(4)),
|
||||
("waveMotion/velocity", n(20.0)),
|
||||
],
|
||||
vec![("waveType", e(1)), ("imageContour/sourceLayer", Value::Layer(Some(MAP))), ("waveStroke/fadeOutTime", n(1.0))],
|
||||
vec![
|
||||
("waveType", e(2)),
|
||||
("waveMask/mask", n(1.0)),
|
||||
("waveMotion/reflection", b(true)),
|
||||
("waveMotion/velocity", n(40.0)),
|
||||
("waveMotion/expansion", n(15.0)),
|
||||
],
|
||||
],
|
||||
),
|
||||
(
|
||||
"ec.generate.vegas",
|
||||
vec![
|
||||
vec![],
|
||||
vec![("stroke", e(1)), ("path", n(3.0)), ("blendMode", e(2)), ("segments", n(12.0)), ("randomPhase", b(true))],
|
||||
vec![("inputLayer", Value::Layer(Some(MAP))), ("blendMode", e(3)), ("shorterContoursHave", e(1)), ("threshold", n(30.0))],
|
||||
],
|
||||
),
|
||||
(
|
||||
"ec.generate.stroke",
|
||||
vec![
|
||||
vec![],
|
||||
vec![("allMasks", b(true)), ("spacing", n(80.0)), ("paintStyle", e(1)), ("end", n(70.0)), ("brushSize", n(6.0))],
|
||||
vec![("path", n(3.0)), ("paintStyle", e(2)), ("start", n(20.0)), ("brushHardness", n(10.0))],
|
||||
],
|
||||
),
|
||||
(
|
||||
"ec.generate.scribble",
|
||||
vec![
|
||||
vec![],
|
||||
vec![("scribble", e(1)), ("fillType", e(2)), ("edgeOptions/endCap", e(2)), ("edgeOptions/join", e(1)), ("composite", e(0))],
|
||||
vec![("mask", n(2.0)), ("fillType", e(1)), ("edgeOptions/join", e(2)), ("wiggleType", e(2)), ("end", n(60.0)), ("composite", e(2))],
|
||||
],
|
||||
),
|
||||
("ec.generate.writeon", vec![vec![], vec![("brushPosition", pt(20.0, 30.0)), ("brushSize", n(14.0)), ("paintStyle", e(1))]]),
|
||||
(
|
||||
"ec.generate.paintbucket",
|
||||
vec![
|
||||
vec![],
|
||||
vec![("fillPoint", pt(10.0, 10.0)), ("fillSelector", e(1)), ("tolerance", n(60.0)), ("stroke", e(4)), ("strokeWidth", n(4.0))],
|
||||
vec![("fillPoint", pt(40.0, 20.0)), ("fillSelector", e(2)), ("stroke", e(1)), ("color", c(0.2, 0.9, 0.4))],
|
||||
vec![("stroke", e(2)), ("spreadRadius", n(2.5)), ("tolerance", n(120.0)), ("viewThreshold", b(false))],
|
||||
],
|
||||
),
|
||||
(
|
||||
"ec.generate.eyedropperfill",
|
||||
vec![
|
||||
vec![],
|
||||
vec![("samplePoint", pt(12.0, 30.0)), ("sampleRadius", n(6.0)), ("averagePixelColors", e(3)), ("blendWithOriginal", n(30.0))],
|
||||
vec![("sampleRadius", n(9.0)), ("averagePixelColors", e(1)), ("maintainOriginalAlpha", b(true))],
|
||||
],
|
||||
),
|
||||
(
|
||||
"ec.generate.audiospectrum",
|
||||
vec![
|
||||
vec![("audioLayer", Value::Layer(Some(MAP)))],
|
||||
vec![("audioLayer", Value::Layer(Some(MAP))), ("displayOptions", e(1)), ("sideOptions", e(2)), ("compositeOnOriginal", b(true))],
|
||||
vec![("audioLayer", Value::Layer(Some(MAP))), ("displayOptions", e(2)), ("path", n(3.0)), ("usePolarPath", b(false))],
|
||||
],
|
||||
),
|
||||
(
|
||||
"ec.generate.audiowaveform",
|
||||
vec![
|
||||
vec![("audioLayer", Value::Layer(Some(MAP)))],
|
||||
vec![("audioLayer", Value::Layer(Some(MAP))), ("displayOptions", e(0)), ("waveformOptions", e(2)), ("usePolarPath", b(true))],
|
||||
vec![("audioLayer", Value::Layer(Some(MAP))), ("displayOptions", e(2)), ("path", n(1.0)), ("compositeOnOriginal", b(true))],
|
||||
],
|
||||
),
|
||||
(
|
||||
"ec.obsolete.basictext",
|
||||
vec![
|
||||
vec![],
|
||||
vec![("text", Value::Str("Hi\nthere".into())), ("fillAndStroke/displayOptions", e(2)), ("alignment", e(0)), ("size", n(14.0))],
|
||||
vec![("fillAndStroke/displayOptions", e(3)), ("fillAndStroke/strokeWidth", n(5.0)), ("compositeOnOriginal", b(true))],
|
||||
],
|
||||
),
|
||||
(
|
||||
"ec.obsolete.pathtext",
|
||||
vec![
|
||||
vec![],
|
||||
vec![("pathOptions/shapeType", e(1)), ("advanced/visibleCharacters", n(2.5)), ("advanced/fadeTime", n(50.0)), ("character/size", n(14.0))],
|
||||
vec![
|
||||
("pathOptions/customPath", n(3.0)),
|
||||
("fillAndStroke/displayOptions", e(2)),
|
||||
("advanced/jitterSettings/rotationJitterMax", n(20.0)),
|
||||
("compositeOnOriginal", b(true)),
|
||||
],
|
||||
],
|
||||
),
|
||||
(
|
||||
"ec.key.innerouter",
|
||||
vec![
|
||||
vec![("foreground", e(1))],
|
||||
vec![("foreground", e(1)), ("background", e(2)), ("edgeFeather", n(3.0)), ("edgeThin", n(1.5))],
|
||||
vec![("foreground", e(1)), ("singleMaskHighlightRadius", n(4.0)), ("invertExtraction", b(true)), ("blendWithOriginal", n(20.0))],
|
||||
],
|
||||
),
|
||||
(
|
||||
"ec.color.colorlink",
|
||||
vec![
|
||||
vec![],
|
||||
vec![("sampleSource", e(2)), ("clip", n(10.0)), ("blendingMode", e(3))],
|
||||
vec![("sourceLayer", Value::Layer(Some(MAP))), ("sampleSource", e(1))],
|
||||
vec![("sampleSource", e(9)), ("opacity", n(70.0))],
|
||||
],
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
/// Layer times.
|
||||
const TIMES: [f64; 2] = [0.4, 1.3];
|
||||
|
||||
fn fnv(img: &Image) -> u64 {
|
||||
let mut h = 0xcbf2_9ce4_8422_2325u64;
|
||||
for b in [img.width, img.height].iter().flat_map(|v| v.to_le_bytes()) {
|
||||
h = (h ^ b as u64).wrapping_mul(0x100_0000_01b3);
|
||||
}
|
||||
for p in &img.data {
|
||||
for c in p {
|
||||
for b in c.to_bits().to_le_bytes() {
|
||||
h = (h ^ b as u64).wrapping_mul(0x100_0000_01b3);
|
||||
}
|
||||
}
|
||||
}
|
||||
h
|
||||
}
|
||||
|
||||
fn render(id: &str, set: &Set, time: f64, bpc8: bool, half: bool) -> Image {
|
||||
let spec = find(id).unwrap_or_else(|| panic!("no effect {id}"));
|
||||
let size = [W as f64, H as f64];
|
||||
let mut params = Params { values: spec.params.iter().map(|p| (p.id.to_string(), default_value(p, size))).collect() };
|
||||
for (k, v) in set {
|
||||
assert!(params.values.contains_key(*k), "{id}: unknown param {k}");
|
||||
params.values.insert(k.to_string(), v.clone());
|
||||
}
|
||||
let mut img = layer(!bpc8);
|
||||
if bpc8 {
|
||||
img.clamp01();
|
||||
img.quantize(255.0);
|
||||
}
|
||||
let mut buf = Buf { img, offset: [0.0; 2], scale: 1.0 };
|
||||
if half {
|
||||
// A half-resolution preview buffer padded by 3 px.
|
||||
let (w2, h2) = (W / 2, H / 2);
|
||||
let mut small = Image::new(w2, h2);
|
||||
for y in 0..h2 {
|
||||
for x in 0..w2 {
|
||||
small.set(x, y, buf.img.sample_bilinear(x as f64 * 2.0 + 1.0, y as f64 * 2.0 + 1.0));
|
||||
}
|
||||
}
|
||||
buf = Buf { img: small, offset: [0.0; 2], scale: 0.5 };
|
||||
buf.pad(3);
|
||||
}
|
||||
let host = Host;
|
||||
let masks = masks();
|
||||
let env = EffectEnv { host: Some(&host), masks: &masks, frame_rate: 30.0, comp_time: time, ..Default::default() };
|
||||
let ctx = EffectCtx { params: ¶ms, time, layer_size: size, seed: 1, adjustment: false, env };
|
||||
let mut out = apply(spec, &ctx, buf).img;
|
||||
if bpc8 {
|
||||
out.clamp01();
|
||||
out.quantize(255.0);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Every case's name and hash, in a fixed order.
|
||||
fn hashes() -> Vec<(String, u64)> {
|
||||
let mut v = Vec::new();
|
||||
for (id, sets) in cases() {
|
||||
for (si, set) in sets.iter().enumerate() {
|
||||
for (bpc8, half) in [(true, false), (false, false), (false, true)] {
|
||||
for t in TIMES {
|
||||
let img = render(id, set, t, bpc8, half);
|
||||
let name = format!("{id}/{si}/{}{}/t{t}", if bpc8 { "8bpc" } else { "32bpc" }, if half { "/half" } else { "" });
|
||||
v.push((name, fnv(&img)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plan_effects_golden_hashes() {
|
||||
let got = hashes();
|
||||
if std::env::var_os("GEN_GOLDEN_PRINT").is_some() {
|
||||
for (name, h) in &got {
|
||||
println!(" (\"{name}\", 0x{h:016x}),");
|
||||
}
|
||||
}
|
||||
assert_eq!(got, hashes(), "the effects render deterministically");
|
||||
if cfg!(all(target_os = "macos", target_arch = "aarch64")) {
|
||||
let bad: Vec<String> = got
|
||||
.iter()
|
||||
.zip(GOLDEN)
|
||||
.filter(|((n, h), (gn, gh))| n != gn || h != gh)
|
||||
.map(|((n, h), (_, gh))| format!("{n}: 0x{h:016x} (pinned 0x{gh:016x})"))
|
||||
.collect();
|
||||
assert_eq!(got.len(), GOLDEN.len(), "case count");
|
||||
assert!(bad.is_empty(), "CPU output changed:\n{}", bad.join("\n"));
|
||||
}
|
||||
}
|
||||
|
||||
/// Pinned hashes (aarch64 macOS).
|
||||
const GOLDEN: &[(&str, u64)] = &[
|
||||
("ec.obsolete.lightning/0/8bpc/t0.4", 0xd829bb3a79ec2d61),
|
||||
("ec.obsolete.lightning/0/8bpc/t1.3", 0xb2221364c0defe9b),
|
||||
("ec.obsolete.lightning/0/32bpc/t0.4", 0x828cc4e53d579af7),
|
||||
("ec.obsolete.lightning/0/32bpc/t1.3", 0x15770238cde9e766),
|
||||
("ec.obsolete.lightning/0/32bpc/half/t0.4", 0xf64f2b87fa9a09d3),
|
||||
("ec.obsolete.lightning/0/32bpc/half/t1.3", 0x625a00bd52fde5aa),
|
||||
("ec.obsolete.lightning/1/8bpc/t0.4", 0xa7c8cb94beebca84),
|
||||
("ec.obsolete.lightning/1/8bpc/t1.3", 0x6e583b1d1f455099),
|
||||
("ec.obsolete.lightning/1/32bpc/t0.4", 0x2ffd973cc7de98f7),
|
||||
("ec.obsolete.lightning/1/32bpc/t1.3", 0x8f35e6018cac57d7),
|
||||
("ec.obsolete.lightning/1/32bpc/half/t0.4", 0x96c14e911783f8d6),
|
||||
("ec.obsolete.lightning/1/32bpc/half/t1.3", 0x620ee605f56c2068),
|
||||
("ec.obsolete.lightning/2/8bpc/t0.4", 0x119e5cedac21cd9b),
|
||||
("ec.obsolete.lightning/2/8bpc/t1.3", 0x04b775fba837ddce),
|
||||
("ec.obsolete.lightning/2/32bpc/t0.4", 0xfee767091adac4ce),
|
||||
("ec.obsolete.lightning/2/32bpc/t1.3", 0x2ae26f9670c93751),
|
||||
("ec.obsolete.lightning/2/32bpc/half/t0.4", 0x99111f77bf26704f),
|
||||
("ec.obsolete.lightning/2/32bpc/half/t1.3", 0x8eb781efc14183c3),
|
||||
("ec.generate.advancedlightning/0/8bpc/t0.4", 0x5a53bf1e0236225d),
|
||||
("ec.generate.advancedlightning/0/8bpc/t1.3", 0x5a53bf1e0236225d),
|
||||
("ec.generate.advancedlightning/0/32bpc/t0.4", 0x6817bf5b3583e0a0),
|
||||
("ec.generate.advancedlightning/0/32bpc/t1.3", 0x6817bf5b3583e0a0),
|
||||
("ec.generate.advancedlightning/0/32bpc/half/t0.4", 0x5dcc055f6cca9922),
|
||||
("ec.generate.advancedlightning/0/32bpc/half/t1.3", 0x5dcc055f6cca9922),
|
||||
("ec.generate.advancedlightning/1/8bpc/t0.4", 0xf07f11457fa683d0),
|
||||
("ec.generate.advancedlightning/1/8bpc/t1.3", 0xf07f11457fa683d0),
|
||||
("ec.generate.advancedlightning/1/32bpc/t0.4", 0x1b21531df9187431),
|
||||
("ec.generate.advancedlightning/1/32bpc/t1.3", 0x1b21531df9187431),
|
||||
("ec.generate.advancedlightning/1/32bpc/half/t0.4", 0xf1032a0a9345edf1),
|
||||
("ec.generate.advancedlightning/1/32bpc/half/t1.3", 0xf1032a0a9345edf1),
|
||||
("ec.generate.advancedlightning/2/8bpc/t0.4", 0x62b5d179f05c6a4c),
|
||||
("ec.generate.advancedlightning/2/8bpc/t1.3", 0x62b5d179f05c6a4c),
|
||||
("ec.generate.advancedlightning/2/32bpc/t0.4", 0x192256fe81f73975),
|
||||
("ec.generate.advancedlightning/2/32bpc/t1.3", 0x192256fe81f73975),
|
||||
("ec.generate.advancedlightning/2/32bpc/half/t0.4", 0x953dd51008c3ce31),
|
||||
("ec.generate.advancedlightning/2/32bpc/half/t1.3", 0x953dd51008c3ce31),
|
||||
("ec.generate.advancedlightning/3/8bpc/t0.4", 0x57b6ad61bd3d89e0),
|
||||
("ec.generate.advancedlightning/3/8bpc/t1.3", 0x57b6ad61bd3d89e0),
|
||||
("ec.generate.advancedlightning/3/32bpc/t0.4", 0x7e7c9d20e94c8b6c),
|
||||
("ec.generate.advancedlightning/3/32bpc/t1.3", 0x7e7c9d20e94c8b6c),
|
||||
("ec.generate.advancedlightning/3/32bpc/half/t0.4", 0xa37940f5a490a0fe),
|
||||
("ec.generate.advancedlightning/3/32bpc/half/t1.3", 0xa37940f5a490a0fe),
|
||||
("ec.generate.radiowaves/0/8bpc/t0.4", 0xbc3643b8af12c44b),
|
||||
("ec.generate.radiowaves/0/8bpc/t1.3", 0x703e948dc562e132),
|
||||
("ec.generate.radiowaves/0/32bpc/t0.4", 0x2b0db208ee7517c1),
|
||||
("ec.generate.radiowaves/0/32bpc/t1.3", 0x7b32484a1fec7db3),
|
||||
("ec.generate.radiowaves/0/32bpc/half/t0.4", 0x166c2672959953cb),
|
||||
("ec.generate.radiowaves/0/32bpc/half/t1.3", 0x5820255022c11f22),
|
||||
("ec.generate.radiowaves/1/8bpc/t0.4", 0x7b74eaa4d0501c4e),
|
||||
("ec.generate.radiowaves/1/8bpc/t1.3", 0xcab53207ecf01165),
|
||||
("ec.generate.radiowaves/1/32bpc/t0.4", 0x433e2457d21bfe74),
|
||||
("ec.generate.radiowaves/1/32bpc/t1.3", 0x2b2c3b2284170a7d),
|
||||
("ec.generate.radiowaves/1/32bpc/half/t0.4", 0x9752bb53cb40c823),
|
||||
("ec.generate.radiowaves/1/32bpc/half/t1.3", 0x8641d37e339914fe),
|
||||
("ec.generate.radiowaves/2/8bpc/t0.4", 0x8ad88caae775ea83),
|
||||
("ec.generate.radiowaves/2/8bpc/t1.3", 0x1f32a9a4a45ff7b3),
|
||||
("ec.generate.radiowaves/2/32bpc/t0.4", 0xe2b8332b96a1c76e),
|
||||
("ec.generate.radiowaves/2/32bpc/t1.3", 0x60f6e6b2a4f5e466),
|
||||
("ec.generate.radiowaves/2/32bpc/half/t0.4", 0x60e6c05f8f8fe967),
|
||||
("ec.generate.radiowaves/2/32bpc/half/t1.3", 0x6fcd7f86aedfbf1d),
|
||||
("ec.generate.radiowaves/3/8bpc/t0.4", 0xa52d7f06548c7d3c),
|
||||
("ec.generate.radiowaves/3/8bpc/t1.3", 0x7a3a0bed3bdd53cf),
|
||||
("ec.generate.radiowaves/3/32bpc/t0.4", 0xbcab47ad7445c601),
|
||||
("ec.generate.radiowaves/3/32bpc/t1.3", 0x940a18e2bd95f75e),
|
||||
("ec.generate.radiowaves/3/32bpc/half/t0.4", 0xb485746038429450),
|
||||
("ec.generate.radiowaves/3/32bpc/half/t1.3", 0xcf62c191e2aa8cc4),
|
||||
("ec.generate.vegas/0/8bpc/t0.4", 0x5e58d44254e9101b),
|
||||
("ec.generate.vegas/0/8bpc/t1.3", 0x5e58d44254e9101b),
|
||||
("ec.generate.vegas/0/32bpc/t0.4", 0xe18712686e402759),
|
||||
("ec.generate.vegas/0/32bpc/t1.3", 0xe18712686e402759),
|
||||
("ec.generate.vegas/0/32bpc/half/t0.4", 0xb037c97234bbd936),
|
||||
("ec.generate.vegas/0/32bpc/half/t1.3", 0xb037c97234bbd936),
|
||||
("ec.generate.vegas/1/8bpc/t0.4", 0x68566eb5dc411cd3),
|
||||
("ec.generate.vegas/1/8bpc/t1.3", 0x68566eb5dc411cd3),
|
||||
("ec.generate.vegas/1/32bpc/t0.4", 0xaacea0d8fbb176ae),
|
||||
("ec.generate.vegas/1/32bpc/t1.3", 0xaacea0d8fbb176ae),
|
||||
("ec.generate.vegas/1/32bpc/half/t0.4", 0xe2f8f5f4a12628d6),
|
||||
("ec.generate.vegas/1/32bpc/half/t1.3", 0xe2f8f5f4a12628d6),
|
||||
("ec.generate.vegas/2/8bpc/t0.4", 0x002c48a91aea2dcf),
|
||||
("ec.generate.vegas/2/8bpc/t1.3", 0x002c48a91aea2dcf),
|
||||
("ec.generate.vegas/2/32bpc/t0.4", 0x4f550bbbf8b8316b),
|
||||
("ec.generate.vegas/2/32bpc/t1.3", 0x4f550bbbf8b8316b),
|
||||
("ec.generate.vegas/2/32bpc/half/t0.4", 0x30036df3e56a01ed),
|
||||
("ec.generate.vegas/2/32bpc/half/t1.3", 0x30036df3e56a01ed),
|
||||
("ec.generate.stroke/0/8bpc/t0.4", 0xa580fbfb806d655c),
|
||||
("ec.generate.stroke/0/8bpc/t1.3", 0xa580fbfb806d655c),
|
||||
("ec.generate.stroke/0/32bpc/t0.4", 0xbec2fe1ec01e1d3c),
|
||||
("ec.generate.stroke/0/32bpc/t1.3", 0xbec2fe1ec01e1d3c),
|
||||
("ec.generate.stroke/0/32bpc/half/t0.4", 0x7c6ae5abc7d0f6e4),
|
||||
("ec.generate.stroke/0/32bpc/half/t1.3", 0x7c6ae5abc7d0f6e4),
|
||||
("ec.generate.stroke/1/8bpc/t0.4", 0x4c9ff27861fe68dd),
|
||||
("ec.generate.stroke/1/8bpc/t1.3", 0x4c9ff27861fe68dd),
|
||||
("ec.generate.stroke/1/32bpc/t0.4", 0xfd1f31366b8d5c1d),
|
||||
("ec.generate.stroke/1/32bpc/t1.3", 0xfd1f31366b8d5c1d),
|
||||
("ec.generate.stroke/1/32bpc/half/t0.4", 0x4dd9dbb22169eae5),
|
||||
("ec.generate.stroke/1/32bpc/half/t1.3", 0x4dd9dbb22169eae5),
|
||||
("ec.generate.stroke/2/8bpc/t0.4", 0x4caead423d6d5ecf),
|
||||
("ec.generate.stroke/2/8bpc/t1.3", 0x4caead423d6d5ecf),
|
||||
("ec.generate.stroke/2/32bpc/t0.4", 0x9ad6539da7e9f26f),
|
||||
("ec.generate.stroke/2/32bpc/t1.3", 0x9ad6539da7e9f26f),
|
||||
("ec.generate.stroke/2/32bpc/half/t0.4", 0xe6d239affe36da4e),
|
||||
("ec.generate.stroke/2/32bpc/half/t1.3", 0xe6d239affe36da4e),
|
||||
("ec.generate.scribble/0/8bpc/t0.4", 0x2e4cbde3ccd30e11),
|
||||
("ec.generate.scribble/0/8bpc/t1.3", 0xe021ad3960135205),
|
||||
("ec.generate.scribble/0/32bpc/t0.4", 0xce10491af6674d85),
|
||||
("ec.generate.scribble/0/32bpc/t1.3", 0x6e9278119b86a775),
|
||||
("ec.generate.scribble/0/32bpc/half/t0.4", 0x968e88723205ae41),
|
||||
("ec.generate.scribble/0/32bpc/half/t1.3", 0x2f69d6431024b969),
|
||||
("ec.generate.scribble/1/8bpc/t0.4", 0x21e7b74d0c7426a1),
|
||||
("ec.generate.scribble/1/8bpc/t1.3", 0x38fe4c9a597fbe00),
|
||||
("ec.generate.scribble/1/32bpc/t0.4", 0xbd6add569300ad1a),
|
||||
("ec.generate.scribble/1/32bpc/t1.3", 0xadfa4f7102cadd9a),
|
||||
("ec.generate.scribble/1/32bpc/half/t0.4", 0x2924a0669270a714),
|
||||
("ec.generate.scribble/1/32bpc/half/t1.3", 0x33784c624845f913),
|
||||
("ec.generate.scribble/2/8bpc/t0.4", 0x83906d3aacbab171),
|
||||
("ec.generate.scribble/2/8bpc/t1.3", 0xe41b5809995bdf9e),
|
||||
("ec.generate.scribble/2/32bpc/t0.4", 0x8c5321d1274edb78),
|
||||
("ec.generate.scribble/2/32bpc/t1.3", 0x905bdccaf9b98a5c),
|
||||
("ec.generate.scribble/2/32bpc/half/t0.4", 0x9d07d455e6a51d41),
|
||||
("ec.generate.scribble/2/32bpc/half/t1.3", 0xc8cddc8a88c8024b),
|
||||
("ec.generate.writeon/0/8bpc/t0.4", 0x38a58bc3538a16e4),
|
||||
("ec.generate.writeon/0/8bpc/t1.3", 0x38a58bc3538a16e4),
|
||||
("ec.generate.writeon/0/32bpc/t0.4", 0x4d555ffd999b1065),
|
||||
("ec.generate.writeon/0/32bpc/t1.3", 0x4d555ffd999b1065),
|
||||
("ec.generate.writeon/0/32bpc/half/t0.4", 0x8ab7075374b11f07),
|
||||
("ec.generate.writeon/0/32bpc/half/t1.3", 0x8ab7075374b11f07),
|
||||
("ec.generate.writeon/1/8bpc/t0.4", 0x21eaeca83e433c55),
|
||||
("ec.generate.writeon/1/8bpc/t1.3", 0x21eaeca83e433c55),
|
||||
("ec.generate.writeon/1/32bpc/t0.4", 0x15d5c8948781d675),
|
||||
("ec.generate.writeon/1/32bpc/t1.3", 0x15d5c8948781d675),
|
||||
("ec.generate.writeon/1/32bpc/half/t0.4", 0x7b849d248824de9d),
|
||||
("ec.generate.writeon/1/32bpc/half/t1.3", 0x7b849d248824de9d),
|
||||
("ec.generate.paintbucket/0/8bpc/t0.4", 0x41390203c012ade6),
|
||||
("ec.generate.paintbucket/0/8bpc/t1.3", 0x41390203c012ade6),
|
||||
("ec.generate.paintbucket/0/32bpc/t0.4", 0x2cc586c148219d9d),
|
||||
("ec.generate.paintbucket/0/32bpc/t1.3", 0x2cc586c148219d9d),
|
||||
("ec.generate.paintbucket/0/32bpc/half/t0.4", 0x0e8eedcff76d46c6),
|
||||
("ec.generate.paintbucket/0/32bpc/half/t1.3", 0x0e8eedcff76d46c6),
|
||||
("ec.generate.paintbucket/1/8bpc/t0.4", 0x7577e2e528aa3ab9),
|
||||
("ec.generate.paintbucket/1/8bpc/t1.3", 0x7577e2e528aa3ab9),
|
||||
("ec.generate.paintbucket/1/32bpc/t0.4", 0x62b69ad0e2b4dfd8),
|
||||
("ec.generate.paintbucket/1/32bpc/t1.3", 0x62b69ad0e2b4dfd8),
|
||||
("ec.generate.paintbucket/1/32bpc/half/t0.4", 0xf5ea3b0a7ac224ed),
|
||||
("ec.generate.paintbucket/1/32bpc/half/t1.3", 0xf5ea3b0a7ac224ed),
|
||||
("ec.generate.paintbucket/2/8bpc/t0.4", 0x530d0a248dc22c72),
|
||||
("ec.generate.paintbucket/2/8bpc/t1.3", 0x530d0a248dc22c72),
|
||||
("ec.generate.paintbucket/2/32bpc/t0.4", 0xe1b655f8815e1357),
|
||||
("ec.generate.paintbucket/2/32bpc/t1.3", 0xe1b655f8815e1357),
|
||||
("ec.generate.paintbucket/2/32bpc/half/t0.4", 0xb95b3b205255cd7e),
|
||||
("ec.generate.paintbucket/2/32bpc/half/t1.3", 0xb95b3b205255cd7e),
|
||||
("ec.generate.paintbucket/3/8bpc/t0.4", 0xa7485b7277e03e66),
|
||||
("ec.generate.paintbucket/3/8bpc/t1.3", 0xa7485b7277e03e66),
|
||||
("ec.generate.paintbucket/3/32bpc/t0.4", 0xc8e06c6eed30448b),
|
||||
("ec.generate.paintbucket/3/32bpc/t1.3", 0xc8e06c6eed30448b),
|
||||
("ec.generate.paintbucket/3/32bpc/half/t0.4", 0xfe0d6223c876db05),
|
||||
("ec.generate.paintbucket/3/32bpc/half/t1.3", 0xfe0d6223c876db05),
|
||||
("ec.generate.eyedropperfill/0/8bpc/t0.4", 0x86248fa83ab166d5),
|
||||
("ec.generate.eyedropperfill/0/8bpc/t1.3", 0x86248fa83ab166d5),
|
||||
("ec.generate.eyedropperfill/0/32bpc/t0.4", 0x5e9ef63fca8086d5),
|
||||
("ec.generate.eyedropperfill/0/32bpc/t1.3", 0x5e9ef63fca8086d5),
|
||||
("ec.generate.eyedropperfill/0/32bpc/half/t0.4", 0x1b6649c0688d829d),
|
||||
("ec.generate.eyedropperfill/0/32bpc/half/t1.3", 0x1b6649c0688d829d),
|
||||
("ec.generate.eyedropperfill/1/8bpc/t0.4", 0x8fd6bb78816570cc),
|
||||
("ec.generate.eyedropperfill/1/8bpc/t1.3", 0x8fd6bb78816570cc),
|
||||
("ec.generate.eyedropperfill/1/32bpc/t0.4", 0x30d8489019c071fe),
|
||||
("ec.generate.eyedropperfill/1/32bpc/t1.3", 0x30d8489019c071fe),
|
||||
("ec.generate.eyedropperfill/1/32bpc/half/t0.4", 0x13a90c31c89568ea),
|
||||
("ec.generate.eyedropperfill/1/32bpc/half/t1.3", 0x13a90c31c89568ea),
|
||||
("ec.generate.eyedropperfill/2/8bpc/t0.4", 0xbfe145e68fb67e0f),
|
||||
("ec.generate.eyedropperfill/2/8bpc/t1.3", 0xbfe145e68fb67e0f),
|
||||
("ec.generate.eyedropperfill/2/32bpc/t0.4", 0x500e27795935722a),
|
||||
("ec.generate.eyedropperfill/2/32bpc/t1.3", 0x500e27795935722a),
|
||||
("ec.generate.eyedropperfill/2/32bpc/half/t0.4", 0x480fd6a293c6597c),
|
||||
("ec.generate.eyedropperfill/2/32bpc/half/t1.3", 0x480fd6a293c6597c),
|
||||
("ec.generate.audiospectrum/0/8bpc/t0.4", 0xb33f32432e79920c),
|
||||
("ec.generate.audiospectrum/0/8bpc/t1.3", 0xb8256632165ce18a),
|
||||
("ec.generate.audiospectrum/0/32bpc/t0.4", 0xf98f14d21a3c442e),
|
||||
("ec.generate.audiospectrum/0/32bpc/t1.3", 0x18211baf9f0d4d41),
|
||||
("ec.generate.audiospectrum/0/32bpc/half/t0.4", 0xa8d8b213c0f7d53e),
|
||||
("ec.generate.audiospectrum/0/32bpc/half/t1.3", 0xc4fe7346c0da92b8),
|
||||
("ec.generate.audiospectrum/1/8bpc/t0.4", 0xf37ca291072917fa),
|
||||
("ec.generate.audiospectrum/1/8bpc/t1.3", 0xb59f0c33274846b9),
|
||||
("ec.generate.audiospectrum/1/32bpc/t0.4", 0xaf6d6567f830fc1e),
|
||||
("ec.generate.audiospectrum/1/32bpc/t1.3", 0x48ba15c0c33741c5),
|
||||
("ec.generate.audiospectrum/1/32bpc/half/t0.4", 0xaceadbc6ccabd6dc),
|
||||
("ec.generate.audiospectrum/1/32bpc/half/t1.3", 0xc62932f485afd259),
|
||||
("ec.generate.audiospectrum/2/8bpc/t0.4", 0x20267f8c48b2a0c4),
|
||||
("ec.generate.audiospectrum/2/8bpc/t1.3", 0xaf6447ee94fe284c),
|
||||
("ec.generate.audiospectrum/2/32bpc/t0.4", 0x6850f20c7cfea994),
|
||||
("ec.generate.audiospectrum/2/32bpc/t1.3", 0x7fc36e46f726f3f4),
|
||||
("ec.generate.audiospectrum/2/32bpc/half/t0.4", 0x96e53e6547ea4822),
|
||||
("ec.generate.audiospectrum/2/32bpc/half/t1.3", 0x69643e75820123ef),
|
||||
("ec.generate.audiowaveform/0/8bpc/t0.4", 0x68a7733a023bc9f1),
|
||||
("ec.generate.audiowaveform/0/8bpc/t1.3", 0x57e13a1aaa458b14),
|
||||
("ec.generate.audiowaveform/0/32bpc/t0.4", 0xb78e946d794b5ade),
|
||||
("ec.generate.audiowaveform/0/32bpc/t1.3", 0x2acdbc2a2558c847),
|
||||
("ec.generate.audiowaveform/0/32bpc/half/t0.4", 0x99da8b92d697e171),
|
||||
("ec.generate.audiowaveform/0/32bpc/half/t1.3", 0xdfe01c4d3368ea92),
|
||||
("ec.generate.audiowaveform/1/8bpc/t0.4", 0x5f463552fd80eda6),
|
||||
("ec.generate.audiowaveform/1/8bpc/t1.3", 0x4e724e55141cb0e2),
|
||||
("ec.generate.audiowaveform/1/32bpc/t0.4", 0xcba797b9142cc9bc),
|
||||
("ec.generate.audiowaveform/1/32bpc/t1.3", 0x1efc192f83ff114f),
|
||||
("ec.generate.audiowaveform/1/32bpc/half/t0.4", 0x5f81670776ddb569),
|
||||
("ec.generate.audiowaveform/1/32bpc/half/t1.3", 0xf56e5d8f8445125f),
|
||||
("ec.generate.audiowaveform/2/8bpc/t0.4", 0xb9fb312b8553ee47),
|
||||
("ec.generate.audiowaveform/2/8bpc/t1.3", 0x08640b88a8496eb1),
|
||||
("ec.generate.audiowaveform/2/32bpc/t0.4", 0x12a7502643511c69),
|
||||
("ec.generate.audiowaveform/2/32bpc/t1.3", 0xc5f3b7bc698f6328),
|
||||
("ec.generate.audiowaveform/2/32bpc/half/t0.4", 0x2409e86ae0f5e841),
|
||||
("ec.generate.audiowaveform/2/32bpc/half/t1.3", 0x5518084e651af2c6),
|
||||
("ec.obsolete.basictext/0/8bpc/t0.4", 0x97a4c8907a44ab03),
|
||||
("ec.obsolete.basictext/0/8bpc/t1.3", 0x97a4c8907a44ab03),
|
||||
("ec.obsolete.basictext/0/32bpc/t0.4", 0x79bfd2ba4e76c65e),
|
||||
("ec.obsolete.basictext/0/32bpc/t1.3", 0x79bfd2ba4e76c65e),
|
||||
("ec.obsolete.basictext/0/32bpc/half/t0.4", 0xd1c0bf17ed973ec3),
|
||||
("ec.obsolete.basictext/0/32bpc/half/t1.3", 0xd1c0bf17ed973ec3),
|
||||
("ec.obsolete.basictext/1/8bpc/t0.4", 0xa3457f3a34ff4655),
|
||||
("ec.obsolete.basictext/1/8bpc/t1.3", 0xa3457f3a34ff4655),
|
||||
("ec.obsolete.basictext/1/32bpc/t0.4", 0x2583d7ce25436fe8),
|
||||
("ec.obsolete.basictext/1/32bpc/t1.3", 0x2583d7ce25436fe8),
|
||||
("ec.obsolete.basictext/1/32bpc/half/t0.4", 0x2c08ade06e4e7bbc),
|
||||
("ec.obsolete.basictext/1/32bpc/half/t1.3", 0x2c08ade06e4e7bbc),
|
||||
("ec.obsolete.basictext/2/8bpc/t0.4", 0xfce091451c0ac93a),
|
||||
("ec.obsolete.basictext/2/8bpc/t1.3", 0xfce091451c0ac93a),
|
||||
("ec.obsolete.basictext/2/32bpc/t0.4", 0x86cd4111d46f6bf8),
|
||||
("ec.obsolete.basictext/2/32bpc/t1.3", 0x86cd4111d46f6bf8),
|
||||
("ec.obsolete.basictext/2/32bpc/half/t0.4", 0xe8c740683bcf309f),
|
||||
("ec.obsolete.basictext/2/32bpc/half/t1.3", 0xe8c740683bcf309f),
|
||||
("ec.obsolete.pathtext/0/8bpc/t0.4", 0x70ca391f2e77dd8b),
|
||||
("ec.obsolete.pathtext/0/8bpc/t1.3", 0x70ca391f2e77dd8b),
|
||||
("ec.obsolete.pathtext/0/32bpc/t0.4", 0x98faf5dd968c859b),
|
||||
("ec.obsolete.pathtext/0/32bpc/t1.3", 0x98faf5dd968c859b),
|
||||
("ec.obsolete.pathtext/0/32bpc/half/t0.4", 0xf903dfeeb23c8394),
|
||||
("ec.obsolete.pathtext/0/32bpc/half/t1.3", 0xf903dfeeb23c8394),
|
||||
("ec.obsolete.pathtext/1/8bpc/t0.4", 0xeac5ee9e29df401c),
|
||||
("ec.obsolete.pathtext/1/8bpc/t1.3", 0xeac5ee9e29df401c),
|
||||
("ec.obsolete.pathtext/1/32bpc/t0.4", 0x2228529a378510e0),
|
||||
("ec.obsolete.pathtext/1/32bpc/t1.3", 0x2228529a378510e0),
|
||||
("ec.obsolete.pathtext/1/32bpc/half/t0.4", 0x15f6edfc65f2baec),
|
||||
("ec.obsolete.pathtext/1/32bpc/half/t1.3", 0x15f6edfc65f2baec),
|
||||
("ec.obsolete.pathtext/2/8bpc/t0.4", 0x3ae68427950162d9),
|
||||
("ec.obsolete.pathtext/2/8bpc/t1.3", 0x3879c008d5655bcc),
|
||||
("ec.obsolete.pathtext/2/32bpc/t0.4", 0xdc9616ff88ce1362),
|
||||
("ec.obsolete.pathtext/2/32bpc/t1.3", 0x0062c20d2a4c9d99),
|
||||
("ec.obsolete.pathtext/2/32bpc/half/t0.4", 0x373d3ede825faeed),
|
||||
("ec.obsolete.pathtext/2/32bpc/half/t1.3", 0xc2680bc43740918a),
|
||||
("ec.key.innerouter/0/8bpc/t0.4", 0x4342d220a1bee59b),
|
||||
("ec.key.innerouter/0/8bpc/t1.3", 0x4342d220a1bee59b),
|
||||
("ec.key.innerouter/0/32bpc/t0.4", 0x7251dbed6a1fafe6),
|
||||
("ec.key.innerouter/0/32bpc/t1.3", 0x7251dbed6a1fafe6),
|
||||
("ec.key.innerouter/0/32bpc/half/t0.4", 0x003941eb2cd18339),
|
||||
("ec.key.innerouter/0/32bpc/half/t1.3", 0x003941eb2cd18339),
|
||||
("ec.key.innerouter/1/8bpc/t0.4", 0xd7bfea3d9550db56),
|
||||
("ec.key.innerouter/1/8bpc/t1.3", 0xd7bfea3d9550db56),
|
||||
("ec.key.innerouter/1/32bpc/t0.4", 0x62f7705e43cc6a71),
|
||||
("ec.key.innerouter/1/32bpc/t1.3", 0x62f7705e43cc6a71),
|
||||
("ec.key.innerouter/1/32bpc/half/t0.4", 0x22f15f770f22f072),
|
||||
("ec.key.innerouter/1/32bpc/half/t1.3", 0x22f15f770f22f072),
|
||||
("ec.key.innerouter/2/8bpc/t0.4", 0x7bdece44f841bb11),
|
||||
("ec.key.innerouter/2/8bpc/t1.3", 0x7bdece44f841bb11),
|
||||
("ec.key.innerouter/2/32bpc/t0.4", 0x5130a1fc09c42120),
|
||||
("ec.key.innerouter/2/32bpc/t1.3", 0x5130a1fc09c42120),
|
||||
("ec.key.innerouter/2/32bpc/half/t0.4", 0x46c0880853618fc8),
|
||||
("ec.key.innerouter/2/32bpc/half/t1.3", 0x46c0880853618fc8),
|
||||
("ec.color.colorlink/0/8bpc/t0.4", 0x539d1da6b2ed744a),
|
||||
("ec.color.colorlink/0/8bpc/t1.3", 0x539d1da6b2ed744a),
|
||||
("ec.color.colorlink/0/32bpc/t0.4", 0x67ed14da67ce1a97),
|
||||
("ec.color.colorlink/0/32bpc/t1.3", 0x67ed14da67ce1a97),
|
||||
("ec.color.colorlink/0/32bpc/half/t0.4", 0x30d29c51622dcc97),
|
||||
("ec.color.colorlink/0/32bpc/half/t1.3", 0x30d29c51622dcc97),
|
||||
("ec.color.colorlink/1/8bpc/t0.4", 0x89ec922f2c8941ed),
|
||||
("ec.color.colorlink/1/8bpc/t1.3", 0x89ec922f2c8941ed),
|
||||
("ec.color.colorlink/1/32bpc/t0.4", 0x684dda31f96ab6fb),
|
||||
("ec.color.colorlink/1/32bpc/t1.3", 0x684dda31f96ab6fb),
|
||||
("ec.color.colorlink/1/32bpc/half/t0.4", 0x93507fbaf4779a8d),
|
||||
("ec.color.colorlink/1/32bpc/half/t1.3", 0x93507fbaf4779a8d),
|
||||
("ec.color.colorlink/2/8bpc/t0.4", 0x8dffbc4fb58aa28d),
|
||||
("ec.color.colorlink/2/8bpc/t1.3", 0x8dffbc4fb58aa28d),
|
||||
("ec.color.colorlink/2/32bpc/t0.4", 0xfcce2f6652e44967),
|
||||
("ec.color.colorlink/2/32bpc/t1.3", 0xfcce2f6652e44967),
|
||||
("ec.color.colorlink/2/32bpc/half/t0.4", 0xfff90f774f305d69),
|
||||
("ec.color.colorlink/2/32bpc/half/t1.3", 0xfff90f774f305d69),
|
||||
("ec.color.colorlink/3/8bpc/t0.4", 0xbbf4d5c846df587a),
|
||||
("ec.color.colorlink/3/8bpc/t1.3", 0xbbf4d5c846df587a),
|
||||
("ec.color.colorlink/3/32bpc/t0.4", 0xa0e34caa39618d1e),
|
||||
("ec.color.colorlink/3/32bpc/t1.3", 0xa0e34caa39618d1e),
|
||||
("ec.color.colorlink/3/32bpc/half/t0.4", 0xd97c6eb3a216573c),
|
||||
("ec.color.colorlink/3/32bpc/half/t1.3", 0xd97c6eb3a216573c),
|
||||
];
|
||||
|
|
@ -325,6 +325,8 @@ impl GpuContext {
|
|||
include_str!("shaders/fx_transition.wgsl"),
|
||||
include_str!("shaders/fx_text.wgsl"),
|
||||
include_str!("shaders/fx_time.wgsl"),
|
||||
include_str!("shaders/fx_pixel2.wgsl"),
|
||||
include_str!("shaders/fx_gen2.wgsl"),
|
||||
include_str!("shaders/sky.wgsl"),
|
||||
]
|
||||
.concat();
|
||||
|
|
@ -394,6 +396,8 @@ impl GpuContext {
|
|||
.chain(crate::fx_transition::KERNELS)
|
||||
.chain(crate::fx_text::KERNELS)
|
||||
.chain(crate::fx_time::KERNELS)
|
||||
.chain(crate::fx_pixel2::KERNELS)
|
||||
.chain(crate::fx_gen2::KERNELS)
|
||||
.chain(crate::adv3d::SKY_KERNELS)
|
||||
.map(|e| (e, &layout))
|
||||
.chain(EXT_ENTRIES.iter().map(|e| (e, &layout_ext)))
|
||||
|
|
|
|||
|
|
@ -176,6 +176,8 @@ fn apply(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
|||
_ if crate::fx_transition::IDS.contains(&id) => crate::fx_transition::apply(e, id, ctx, b),
|
||||
_ if crate::fx_text::IDS.contains(&id) => crate::fx_text::apply(e, id, ctx, b),
|
||||
_ if crate::fx_time::IDS.contains(&id) => crate::fx_time::apply(e, id, ctx, b),
|
||||
_ if crate::fx_pixel2::IDS.contains(&id) => crate::fx_pixel2::apply(e, id, ctx, b),
|
||||
_ if crate::fx_gen2::IDS.contains(&id) => crate::fx_gen2::apply(e, id, ctx, b),
|
||||
_ => pointwise(e, id, ctx, b),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,561 @@
|
|||
//! GPU effects, generators of part C (kernels in `shaders/fx_gen2.wgsl`, every entry point
|
||||
//! prefixed `fg2_`): Lightning, Advanced Lightning, Beam, Fractal, Lens Flare, Radio Waves,
|
||||
//! Vegas, Stroke, Scribble, Write-on, Paint Bucket, Eyedropper Fill, CC Glue Gun, CC Threads,
|
||||
//! Audio Spectrum, Audio Waveform, Basic Text and Path Text.
|
||||
//!
|
||||
//! Geometry stays on the CPU and is shared with the CPU effect as a per-frame plan: the bolts
|
||||
//! (`effects::bolt_plan`, `effects::lightning_segments`), mask strokes, scribbles and contour
|
||||
//! segments (`effects::paint_plan`), audio marks (`effects::marks_plan`: the FFT and peak
|
||||
//! analysis), glyph strokes (`effects::text_passes`), live waves and their contour
|
||||
//! (`effects::radio_plan`), Paint Bucket's flood-filled region and Eyedropper Fill's sampled
|
||||
//! colour (both over the read-back layer). The plans' segments and polygons are binned into
|
||||
//! 16 × 16 tiles on the CPU and rasterised here with the CPU's coverage functions
|
||||
//! (`raster_segs`' smooth round caps, `raster_convex`'s 4 × 4 samples); the colouring, glows
|
||||
//! (util::gauss_plane) and compositing run here too. Beam, Lens Flare, CC Glue Gun and CC
|
||||
//! Threads are closed-form kernels.
|
||||
//!
|
||||
//! Fractal stays on the CPU: its escape iteration needs f64 (WGSL has none, and Metal's fast
|
||||
//! math defeats double-f32 emulation); in f32, orbits near the set escape at other iterations
|
||||
//! on 0.3–1.5 % of the pixels even at low magnification.
|
||||
|
||||
use effectcraft_effects::{Buf, Coverage, EffectCtx};
|
||||
use effectcraft_raster::Image;
|
||||
|
||||
use crate::context::{Enc, GpuImage, Params};
|
||||
use crate::effects::GBuf;
|
||||
use crate::fx_pixel2::gauss_plane;
|
||||
|
||||
/// Compute entry points in `fx_gen2.wgsl`.
|
||||
pub(crate) const KERNELS: &[&str] = &[
|
||||
"fg2_segs",
|
||||
"fg2_convex",
|
||||
"fg2_paint",
|
||||
"fg2_marks",
|
||||
"fg2_pair",
|
||||
"fg2_bolt",
|
||||
"fg2_adv_bolt",
|
||||
"fg2_text",
|
||||
"fg2_radio",
|
||||
"fg2_beam",
|
||||
"fg2_flare",
|
||||
"fg2_glue",
|
||||
"fg2_threads",
|
||||
"fg2_bucket",
|
||||
"fg2_bucket_cov",
|
||||
"fg2_eyedropper",
|
||||
];
|
||||
|
||||
/// Effect ids implemented here.
|
||||
pub(crate) const IDS: &[&str] = &[
|
||||
"ec.obsolete.lightning",
|
||||
"ec.generate.advancedlightning",
|
||||
"ec.generate.beam",
|
||||
"ec.generate.lensflare",
|
||||
"ec.generate.radiowaves",
|
||||
"ec.generate.vegas",
|
||||
"ec.generate.stroke",
|
||||
"ec.generate.scribble",
|
||||
"ec.generate.writeon",
|
||||
"ec.generate.paintbucket",
|
||||
"ec.generate.eyedropperfill",
|
||||
"ec.generate.ccgluegun",
|
||||
"ec.generate.ccthreads",
|
||||
"ec.generate.audiospectrum",
|
||||
"ec.generate.audiowaveform",
|
||||
"ec.obsolete.basictext",
|
||||
"ec.obsolete.pathtext",
|
||||
];
|
||||
|
||||
/// Largest item table (floats) the raster kernels take; bigger plans render on the CPU.
|
||||
const MAX_FLOATS: usize = 1 << 24;
|
||||
const TILE: i32 = 16;
|
||||
/// "No minimum radius" for a segment variant.
|
||||
const NO_MIN: f32 = -3.0e38;
|
||||
|
||||
/// Run effect `id` (one of [`IDS`]); `None` = this parameter combination runs on the CPU.
|
||||
pub(crate) fn apply(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
match id {
|
||||
"ec.obsolete.lightning" => lightning(e, ctx, b),
|
||||
"ec.generate.advancedlightning" => advanced_lightning(e, ctx, b),
|
||||
"ec.generate.beam" => beam(e, ctx, b),
|
||||
"ec.generate.lensflare" => lens_flare(e, ctx, b),
|
||||
"ec.generate.radiowaves" => radio_waves(e, ctx, b),
|
||||
"ec.generate.vegas" | "ec.generate.stroke" | "ec.generate.scribble" | "ec.generate.writeon" => paint(e, id, ctx, b),
|
||||
"ec.generate.paintbucket" => paint_bucket(e, ctx, b),
|
||||
"ec.generate.eyedropperfill" => eyedropper(e, ctx, b),
|
||||
"ec.generate.ccgluegun" => glue_gun(e, ctx, b),
|
||||
"ec.generate.ccthreads" => threads(e, ctx, b),
|
||||
"ec.generate.audiospectrum" | "ec.generate.audiowaveform" => marks(e, id, ctx, b),
|
||||
"ec.obsolete.basictext" | "ec.obsolete.pathtext" => text(e, id, ctx, b),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// A same-size per-pixel kernel over the buffer.
|
||||
fn run(e: &mut Enc, entry: &str, p: &Params, mut b: GBuf, aux: Option<&GpuImage>, data: Option<&wgpu::Buffer>) -> Option<GBuf> {
|
||||
let out = e.scratch(b.img.width, b.img.height);
|
||||
e.pixels(entry, p, &b.img, aux, &out, data);
|
||||
b.img = out;
|
||||
Some(b)
|
||||
}
|
||||
|
||||
/// The buffer on the CPU for building a plan: its pixels when `pixels` (read back), else only
|
||||
/// its geometry.
|
||||
fn cpu_buf(e: &mut Enc, b: &GBuf, pixels: bool) -> Option<Buf> {
|
||||
let img = if pixels { e.download(&b.img)? } else { Image { width: b.img.width, height: b.img.height, data: vec![] } };
|
||||
Some(Buf { img, offset: b.offset, scale: b.scale })
|
||||
}
|
||||
|
||||
fn rgb(c: [f32; 4]) -> [f32; 4] {
|
||||
[c[0], c[1], c[2], 0.0]
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- tiled item raster
|
||||
|
||||
/// Items with their integer pixel bounds, binned into 16 × 16 tiles.
|
||||
struct Items {
|
||||
stride: usize,
|
||||
recs: Vec<f32>,
|
||||
w: i32,
|
||||
h: i32,
|
||||
tiles: Vec<Vec<u32>>,
|
||||
tx: i32,
|
||||
}
|
||||
|
||||
impl Items {
|
||||
fn new(stride: usize, w: u32, h: u32) -> Items {
|
||||
let (w, h) = (w as i32, h as i32);
|
||||
let tx = (w + TILE - 1) / TILE;
|
||||
let ty = (h + TILE - 1) / TILE;
|
||||
Items { stride, recs: vec![], w, h, tiles: vec![vec![]; (tx * ty).max(0) as usize], tx }
|
||||
}
|
||||
|
||||
/// Add an item (`rec` = its fields, without the bounds) covering float bounds `bb`
|
||||
/// ([x0, y0, x1, y1]).
|
||||
fn push(&mut self, bb: [f64; 4], rec: &[f32]) {
|
||||
if !bb.iter().all(|v| v.is_finite()) {
|
||||
return;
|
||||
}
|
||||
let x0 = bb[0].floor().max(0.0) as i32;
|
||||
let y0 = bb[1].floor().max(0.0) as i32;
|
||||
let x1 = (bb[2].ceil().min(self.w as f64 - 1.0)) as i32;
|
||||
let y1 = (bb[3].ceil().min(self.h as f64 - 1.0)) as i32;
|
||||
if x1 < x0 || y1 < y0 || bb[2] < 0.0 || bb[3] < 0.0 {
|
||||
return;
|
||||
}
|
||||
let i = (self.recs.len() / self.stride) as u32;
|
||||
let start = self.recs.len();
|
||||
self.recs.extend_from_slice(rec);
|
||||
self.recs.resize(start + self.stride - 4, 0.0);
|
||||
self.recs.extend([x0 as f32, y0 as f32, x1 as f32, y1 as f32]);
|
||||
for ty in y0 / TILE..=y1 / TILE {
|
||||
for tx in x0 / TILE..=x1 / TILE {
|
||||
self.tiles[(ty * self.tx + tx) as usize].push(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Kernel data (tile offsets, item indices, items), the items' offset and the tile count
|
||||
/// across. `None` when too large.
|
||||
fn data(self) -> Option<(Vec<f32>, u32, u32)> {
|
||||
let n_tiles = self.tiles.len();
|
||||
let n_idx: usize = self.tiles.iter().map(Vec::len).sum();
|
||||
let total = n_tiles + 1 + n_idx + self.recs.len();
|
||||
if total > MAX_FLOATS {
|
||||
return None;
|
||||
}
|
||||
let mut d = Vec::with_capacity(total);
|
||||
let base = (n_tiles + 1) as f32;
|
||||
let mut at = 0usize;
|
||||
for t in &self.tiles {
|
||||
d.push(base + at as f32);
|
||||
at += t.len();
|
||||
}
|
||||
d.push(base + at as f32);
|
||||
for t in &self.tiles {
|
||||
d.extend(t.iter().map(|&i| i as f32));
|
||||
}
|
||||
let items_at = d.len() as u32;
|
||||
d.extend(self.recs);
|
||||
Some((d, items_at, self.tx.max(0) as u32))
|
||||
}
|
||||
}
|
||||
|
||||
/// A coverage of the segment raster: radius max(r · `mul` + `add`, `min`) and hardness
|
||||
/// (generate3::raster_segs), or Advanced Lightning's linear core.
|
||||
#[derive(Clone, Copy)]
|
||||
struct Variant {
|
||||
mul: f64,
|
||||
add: f64,
|
||||
min: f64,
|
||||
hard: f64,
|
||||
linear: bool,
|
||||
}
|
||||
|
||||
impl Variant {
|
||||
fn plain(hard: f64) -> Variant {
|
||||
Variant { mul: 1.0, add: 0.0, min: NO_MIN as f64, hard, linear: false }
|
||||
}
|
||||
|
||||
fn radius(&self, r: f64) -> f64 {
|
||||
(r * self.mul + self.add).max(self.min)
|
||||
}
|
||||
}
|
||||
|
||||
/// Rasterise segments (a, b, radius, value) into up to four coverages (x, y, z, w), one per
|
||||
/// variant, max-combined as raster_segs.
|
||||
fn raster_segs(e: &mut Enc, w: u32, h: u32, segs: impl Iterator<Item = ([f64; 2], [f64; 2], f64, f32)>, variants: &[Variant]) -> Option<GpuImage> {
|
||||
let mut items = Items::new(10, w, h);
|
||||
for (a, z, r, v) in segs {
|
||||
let pad = variants.iter().map(|k| k.radius(r)).fold(f64::NEG_INFINITY, f64::max) + 1.0;
|
||||
let bb = [a[0].min(z[0]) - pad, a[1].min(z[1]) - pad, a[0].max(z[0]) + pad, a[1].max(z[1]) + pad];
|
||||
items.push(bb, &[a[0] as f32, a[1] as f32, z[0] as f32, z[1] as f32, r as f32, v]);
|
||||
}
|
||||
let (data, items_at, tx) = items.data()?;
|
||||
let buf = e.data(&data);
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [items_at, 10, tx, variants.len() as u32];
|
||||
for (k, v) in variants.iter().enumerate() {
|
||||
p.f[k] = [v.mul as f32, v.add as f32, v.min as f32, v.hard as f32];
|
||||
p.u[1][0] |= (v.linear as u32) << k;
|
||||
}
|
||||
let out = e.scratch(w, h);
|
||||
let dummy = e.zeros(1, 1);
|
||||
e.pixels("fg2_segs", &p, &dummy, None, &out, Some(&buf));
|
||||
Some(out)
|
||||
}
|
||||
|
||||
/// A plan's coverage (x).
|
||||
fn coverage(e: &mut Enc, w: u32, h: u32, cov: &Coverage) -> Option<GpuImage> {
|
||||
match cov {
|
||||
Coverage::Segs { segs, hardness } => raster_segs(e, w, h, segs.iter().map(|s| (s.a, s.b, s.r, s.v)), &[Variant::plain(*hardness)]),
|
||||
Coverage::Convex(polys) => {
|
||||
const STRIDE: usize = 1 + 32 + 4;
|
||||
let mut items = Items::new(STRIDE, w, h);
|
||||
for poly in polys {
|
||||
if poly.len() < 3 {
|
||||
continue;
|
||||
}
|
||||
if poly.len() > 16 {
|
||||
return None;
|
||||
}
|
||||
let mut rec = vec![poly.len() as f32];
|
||||
let mut bb = [f64::INFINITY, f64::INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY];
|
||||
for q in poly {
|
||||
rec.extend([q[0] as f32, q[1] as f32]);
|
||||
bb = [bb[0].min(q[0]), bb[1].min(q[1]), bb[2].max(q[0]), bb[3].max(q[1])];
|
||||
}
|
||||
items.push(bb, &rec);
|
||||
}
|
||||
let (data, items_at, tx) = items.data()?;
|
||||
let buf = e.data(&data);
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [items_at, STRIDE as u32, tx, 0];
|
||||
let out = e.scratch(w, h);
|
||||
let dummy = e.zeros(1, 1);
|
||||
e.pixels("fg2_convex", &p, &dummy, None, &out, Some(&buf));
|
||||
Some(out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Stroke, Scribble, Vegas, Write-on
|
||||
|
||||
fn paint(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let cpu = cpu_buf(e, &b, effectcraft_effects::plan_reads_pixels(id, ctx))?;
|
||||
let Some(plan) = effectcraft_effects::paint_plan(id, ctx, &cpu) else {
|
||||
// Scribble without a usable mask: cleared On Transparent, else unchanged.
|
||||
if id == "ec.generate.scribble" && effectcraft_effects::scribble_style(ctx) == 1 {
|
||||
let img = e.image(b.img.width, b.img.height);
|
||||
return Some(GBuf { img, ..b });
|
||||
}
|
||||
return Some(b);
|
||||
};
|
||||
let cov = coverage(e, b.img.width, b.img.height, &plan.cov)?;
|
||||
let mut p = Params::default();
|
||||
p.u[0][0] = plan.style;
|
||||
p.f[0] = plan.color;
|
||||
p.f[1][0] = plan.opacity;
|
||||
run(e, "fg2_paint", &p, b, Some(&cov), None)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Audio Spectrum / Waveform
|
||||
|
||||
fn marks(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let cpu = cpu_buf(e, &b, false)?;
|
||||
let Some(m) = effectcraft_effects::marks_plan(id, ctx, &cpu) else { return Some(b) };
|
||||
let core = Variant::plain(1.0);
|
||||
let halo = if m.softness > 0.0 { Variant { add: m.thickness * m.softness.max(0.0) + 0.5, ..Variant::plain(0.0) } } else { core };
|
||||
let cov = raster_segs(e, b.img.width, b.img.height, m.segs.iter().map(|s| (s.a, s.b, s.r, s.v)), &[core, halo])?;
|
||||
let mut p = Params::default();
|
||||
p.u[0][0] = m.composite as u32;
|
||||
p.f[0] = m.inside;
|
||||
p.f[1] = m.outside;
|
||||
run(e, "fg2_marks", &p, b, Some(&cov), None)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Lightning / Advanced Lightning
|
||||
|
||||
fn lightning(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let cpu = cpu_buf(e, &b, false)?;
|
||||
let plan = effectcraft_effects::bolt_plan(ctx, &cpu);
|
||||
let (w, h) = (b.img.width, b.img.height);
|
||||
let core = Variant { mul: plan.core, min: 0.3, ..Variant::plain(0.6) };
|
||||
let cov = raster_segs(e, w, h, plan.segs.iter().map(|s| (s.a, s.b, s.r, s.v)), &[Variant::plain(0.0), core])?;
|
||||
let glow = gauss_plane(e, &cov, plan.width * 0.5, plan.width * 0.5);
|
||||
let m = e.scratch(w, h);
|
||||
e.pixels("fg2_pair", &Params::default(), &cov, Some(&glow), &m, None);
|
||||
let mut p = Params::default();
|
||||
p.u[0][0] = plan.mode;
|
||||
p.f[0] = rgb(plan.outside);
|
||||
p.f[1] = rgb(plan.inside);
|
||||
run(e, "fg2_bolt", &p, b, Some(&m), None)
|
||||
}
|
||||
|
||||
fn advanced_lightning(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let pr = ctx.params;
|
||||
let cpu = cpu_buf(e, &b, effectcraft_effects::plan_reads_pixels("ec.generate.advancedlightning", ctx))?;
|
||||
let core_r = (pr.f("coreSettings/coreRadius") * b.scale).max(0.3);
|
||||
let glow_r = pr.f("glowSettings/glowRadius") * b.scale;
|
||||
let segs = effectcraft_effects::lightning_segments(ctx, &cpu);
|
||||
let (w, h) = (b.img.width, b.img.height);
|
||||
let linear = Variant { linear: true, ..Variant::plain(0.0) };
|
||||
let core = raster_segs(e, w, h, segs.iter().map(|(a, z, i)| ([a.0, a.1], [z.0, z.1], core_r, *i)), &[linear])?;
|
||||
let glow = glow_r > 0.5;
|
||||
let blurred = if glow { gauss_plane(e, &core, glow_r / 3.0, glow_r / 3.0) } else { core.clone() };
|
||||
let m = e.scratch(w, h);
|
||||
e.pixels("fg2_pair", &Params::default(), &core, Some(&blurred), &m, None);
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [pr.b("compositeOnOriginal") as u32, glow as u32, 0, 0];
|
||||
p.f[0] = [(pr.f("coreSettings/coreOpacity") / 100.0) as f32, (pr.f("glowSettings/glowOpacity") / 100.0) as f32, 0.0, 0.0];
|
||||
p.f[1] = rgb(pr.color("coreSettings/coreColor"));
|
||||
p.f[2] = rgb(pr.color("glowSettings/glowColor"));
|
||||
run(e, "fg2_adv_bolt", &p, b, Some(&m), None)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Basic Text / Path Text
|
||||
|
||||
fn text(e: &mut Enc, id: &str, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
|
||||
let cpu = cpu_buf(e, &b, false)?;
|
||||
let passes = effectcraft_effects::text_passes(id, ctx, &cpu)?;
|
||||
let (w, h) = (b.img.width, b.img.height);
|
||||
for pass in passes {
|
||||
let look = pass.look;
|
||||
let mut segs = Vec::new();
|
||||
for pl in &pass.polys {
|
||||
effectcraft_effects::poly_segs(pl, false, pass.r, 1.0, &mut segs);
|
||||
}
|
||||
let sw = look.stroke_w.max(0.0);
|
||||
let at = |add: f64| Variant { add, min: 0.05, ..Variant::plain(1.0) };
|
||||
let cov = raster_segs(e, w, h, segs.iter().map(|s| (s.a, s.b, s.r, s.v)), &[at(0.0), at(sw * 0.5), at(-sw * 0.5)])?;
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [look.display, look.on_original as u32, (look.display != 0 && sw > 0.0) as u32, (pass.r - sw * 0.5 > 0.05) as u32];
|
||||
p.f[0] = look.fill;
|
||||
p.f[1] = look.stroke;
|
||||
p.f[2][0] = look.opacity;
|
||||
b = run(e, "fg2_text", &p, b, Some(&cov), None)?;
|
||||
}
|
||||
Some(b)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Radio Waves
|
||||
|
||||
fn radio_waves(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let cpu = cpu_buf(e, &b, false)?;
|
||||
let Some(plan) = effectcraft_effects::radio_plan(ctx, &cpu) else { return Some(b) };
|
||||
let n = plan.waves.len();
|
||||
let mut data = Vec::with_capacity(n * 13);
|
||||
let mut outlines = Vec::new();
|
||||
for wv in &plan.waves {
|
||||
let (off, cnt) = match &wv.outline {
|
||||
Some(o) => {
|
||||
let off = n * 13 + outlines.len();
|
||||
outlines.extend(o.iter().flat_map(|q| [q.0 as f32, q.1 as f32]));
|
||||
(off, o.len())
|
||||
}
|
||||
None => (0, 0),
|
||||
};
|
||||
data.extend([
|
||||
wv.centre.0 as f32,
|
||||
wv.centre.1 as f32,
|
||||
wv.radius as f32,
|
||||
wv.half_width as f32,
|
||||
wv.rotation as f32,
|
||||
wv.fade,
|
||||
wv.color[0],
|
||||
wv.color[1],
|
||||
wv.color[2],
|
||||
wv.color[3],
|
||||
wv.opacity,
|
||||
off as f32,
|
||||
cnt as f32,
|
||||
]);
|
||||
}
|
||||
data.extend(outlines);
|
||||
if data.len() > MAX_FLOATS {
|
||||
return None;
|
||||
}
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [n as u32, plan.contour.is_some() as u32, plan.profile, 0];
|
||||
p.f[0][0] = plan.aa as f32;
|
||||
let sdf = match &plan.contour {
|
||||
Some(c) => {
|
||||
let mut img = Image::new(c.sdf.w as u32, c.sdf.h as u32);
|
||||
for (px, &v) in img.data.iter_mut().zip(&c.sdf.data) {
|
||||
px[0] = v;
|
||||
}
|
||||
p.f[0][1] = c.anchor.0 as f32;
|
||||
p.f[0][2] = c.anchor.1 as f32;
|
||||
Some(e.g.upload_image(&img)?)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
let buf = e.data(&data);
|
||||
run(e, "fg2_radio", &p, b, sdf.as_ref(), Some(&buf))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Beam / Lens Flare
|
||||
|
||||
fn beam(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let pr = ctx.params;
|
||||
let s = b.to_px(pr.v2("startPoint"));
|
||||
let en = b.to_px(pr.v2("endPoint"));
|
||||
let len = (pr.f("length") / 100.0).clamp(0.0, 1.0);
|
||||
let time = (pr.f("time") / 100.0).clamp(0.0, 1.0);
|
||||
let t0 = time * (1.0 - len);
|
||||
let (dx, dy) = (en.0 - s.0, en.1 - s.1);
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [pr.b("perspective3d") as u32, pr.b("compositeOnOriginal") as u32, (len > 0.0) as u32, 0];
|
||||
p.f[0] = [s.0 as f32, s.1 as f32, dx as f32, dy as f32];
|
||||
p.f[1] = [t0 as f32, (t0 + len) as f32, (pr.f("startThickness") * b.scale) as f32, (pr.f("endThickness") * b.scale) as f32];
|
||||
p.f[2] = [(pr.f("softness") / 100.0).clamp(0.0, 1.0) as f32, (dx * dx + dy * dy).max(1e-9) as f32, 0.0, 0.0];
|
||||
p.f[3] = rgb(pr.color("insideColor"));
|
||||
p.f[4] = rgb(pr.color("outsideColor"));
|
||||
run(e, "fg2_beam", &p, b, None, None)
|
||||
}
|
||||
|
||||
fn lens_flare(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let pr = ctx.params;
|
||||
let f = b.to_px(pr.v2("flareCenter"));
|
||||
let lens = pr.e("lensType");
|
||||
let (w, h) = (ctx.layer_size[0] * b.scale, ctx.layer_size[1] * b.scale);
|
||||
let c = (b.offset[0] + w * 0.5, b.offset[1] + h * 0.5);
|
||||
let diag = (w * w + h * h).sqrt().max(1.0);
|
||||
let (core_k, rays, halo_r) = match lens {
|
||||
1 => (0.035, 6.0, 0.18),
|
||||
2 => (0.02, 12.0, 0.3),
|
||||
_ => (0.03, 8.0, 0.25),
|
||||
};
|
||||
let ghosts = effectcraft_effects::flare_ghosts(lens);
|
||||
let data: Vec<f32> = ghosts.iter().flat_map(|g| [g.t as f32, g.r as f32, g.c[0], g.c[1], g.c[2], g.k, g.ring as u32 as f32]).collect();
|
||||
let mut p = Params::default();
|
||||
p.u[0][0] = ghosts.len() as u32;
|
||||
p.f[0] = [f.0 as f32, f.1 as f32, c.0 as f32, c.1 as f32];
|
||||
p.f[1] = [diag as f32, core_k, rays, halo_r];
|
||||
p.f[2] = [(pr.f("flareBrightness") / 100.0) as f32, (pr.f("blendWithOriginal") / 100.0) as f32, 0.0, 0.0];
|
||||
let buf = e.data(&data);
|
||||
run(e, "fg2_flare", &p, b, None, Some(&buf))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Glue Gun / CC Threads
|
||||
|
||||
fn glue_gun(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let pr = ctx.params;
|
||||
let q = b.to_px(pr.v2("brushPosition"));
|
||||
let rad = (pr.f("strokeWidth") * b.scale * 0.5).max(0.5);
|
||||
let refl = pr.f("reflection").to_radians();
|
||||
let strength = (pr.f("strength") / 100.0) as f32;
|
||||
let beads = (pr.f("density").max(0.0).round() as usize).clamp(1, 32);
|
||||
let hash = |i: usize, k: u32| effectcraft_raster::hash_noise(i as u32, k, ctx.seed) as f64;
|
||||
// generate3::glue_gun's beads.
|
||||
let mut data = Vec::with_capacity(beads * 3);
|
||||
for i in 0..beads {
|
||||
let a = hash(i, 1) * 2.0 * std::f64::consts::PI;
|
||||
let d = if i == 0 { 0.0 } else { hash(i, 2) * rad * 0.35 };
|
||||
data.extend([(q.0 + a.cos() * d) as f32, (q.1 + a.sin() * d) as f32, (rad * (0.75 + 0.25 * hash(i, 3))) as f32]);
|
||||
}
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [beads as u32, (pr.e("style") == 1) as u32, 0, 0];
|
||||
p.f[0] = [(refl.cos() * 0.6) as f32, (-refl.sin() * 0.6) as f32, 0.53, strength];
|
||||
p.f[1][0] = strength.clamp(0.0, 1.0).max(0.3);
|
||||
let buf = e.data(&data);
|
||||
run(e, "fg2_glue", &p, b, None, Some(&buf))
|
||||
}
|
||||
|
||||
fn threads(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let pr = ctx.params;
|
||||
let sc = b.scale;
|
||||
let (sn, cs) = pr.f("direction").to_radians().sin_cos();
|
||||
let c = b.to_px(pr.v2("center"));
|
||||
let mut p = Params::default();
|
||||
p.u[0][0] = pr.f("overlaps").round().clamp(1.0, 20.0) as u32;
|
||||
p.f[0] = [c.0 as f32, c.1 as f32, sn as f32, cs as f32];
|
||||
p.f[1] = [
|
||||
(pr.f("width") * sc).max(1.0) as f32,
|
||||
(pr.f("height") * sc).max(1.0) as f32,
|
||||
(pr.f("coverage") / 100.0).clamp(0.0, 1.0) as f32,
|
||||
(pr.f("shadowing") / 100.0) as f32,
|
||||
];
|
||||
p.f[2][0] = (pr.f("texture") / 100.0) as f32;
|
||||
run(e, "fg2_threads", &p, b, None, None)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Paint Bucket / Eyedropper Fill
|
||||
|
||||
fn paint_bucket(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let pr = ctx.params;
|
||||
let cpu = cpu_buf(e, &b, true)?;
|
||||
let region = effectcraft_effects::paint_bucket_region(ctx, &cpu);
|
||||
let (w, h) = (b.img.width, b.img.height);
|
||||
let mut img = Image::new(w, h);
|
||||
for (px, &v) in img.data.iter_mut().zip(®ion.data) {
|
||||
px[0] = v;
|
||||
}
|
||||
let region = e.g.upload_image(&img)?;
|
||||
let cov_kernel = |e: &mut Enc, mode: u32, a: &GpuImage, x: &GpuImage| {
|
||||
let mut p = Params::default();
|
||||
p.u[0][0] = mode;
|
||||
let out = e.scratch(w, h);
|
||||
e.pixels("fg2_bucket_cov", &p, a, Some(x), &out, None);
|
||||
out
|
||||
};
|
||||
if pr.b("viewThreshold") {
|
||||
let img = cov_kernel(e, 1, ®ion, ®ion);
|
||||
return Some(GBuf { img, ..b });
|
||||
}
|
||||
let sc = b.scale;
|
||||
use crate::fx_color::{DILATE_X, ERODE_X, morph_frac};
|
||||
let cov = match pr.e("stroke") {
|
||||
1 => {
|
||||
let s = pr.f("featherSoftness") * sc * 0.5;
|
||||
if s > 0.05 { gauss_plane(e, ®ion, s, s) } else { region }
|
||||
}
|
||||
2 => morph_frac(e, ®ion, pr.f("spreadRadius") * sc, DILATE_X),
|
||||
3 => morph_frac(e, ®ion, pr.f("spreadRadius") * sc, ERODE_X),
|
||||
4 => {
|
||||
let r = pr.f("strokeWidth") * sc;
|
||||
let out = morph_frac(e, ®ion, r * 0.5, DILATE_X);
|
||||
let inn = morph_frac(e, ®ion, r * 0.5, ERODE_X);
|
||||
cov_kernel(e, 2, &out, &inn)
|
||||
}
|
||||
_ => {
|
||||
let blurred = gauss_plane(e, ®ion, 0.5, 0.5);
|
||||
cov_kernel(e, 0, ®ion, &blurred)
|
||||
}
|
||||
};
|
||||
let mut p = Params::default();
|
||||
p.u[0][0] = pr.e("blendingMode");
|
||||
p.f[0] = pr.color("color");
|
||||
p.f[1][0] = (pr.f("opacity") / 100.0) as f32;
|
||||
run(e, "fg2_bucket", &p, b, Some(&cov), None)
|
||||
}
|
||||
|
||||
fn eyedropper(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let cpu = cpu_buf(e, &b, true)?;
|
||||
let fill = effectcraft_effects::eyedropper_color(ctx, &cpu);
|
||||
let mut p = Params::default();
|
||||
p.u[0][0] = ctx.params.b("maintainOriginalAlpha") as u32;
|
||||
p.f[0] = fill;
|
||||
p.f[1][0] = (ctx.params.f("blendWithOriginal") / 100.0) as f32;
|
||||
run(e, "fg2_eyedropper", &p, b, None, None)
|
||||
}
|
||||
|
|
@ -88,7 +88,7 @@ fn src_index(s: Src) -> u32 {
|
|||
|
||||
/// Layer parameter `id` resampled into the buffer's pixel grid (`util::fit_layer`):
|
||||
/// `Some(None)` when no layer is chosen (the effect then uses its own layer).
|
||||
fn fitted(e: &mut Enc, ctx: &EffectCtx, b: &GBuf, id: &str, masks_and_effects: bool, stretch: bool) -> Option<Option<GpuImage>> {
|
||||
pub(crate) fn fitted(e: &mut Enc, ctx: &EffectCtx, b: &GBuf, id: &str, masks_and_effects: bool, stretch: bool) -> Option<Option<GpuImage>> {
|
||||
let Some(o) = ctx.layer_param(id, masks_and_effects) else { return Some(None) };
|
||||
let (w, h) = (b.img.width, b.img.height);
|
||||
if o.buf.img.is_empty() {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,518 @@
|
|||
//! GPU effects, pixel ports of part C (kernels in `shaders/fx_pixel2.wgsl`, every entry point
|
||||
//! prefixed `fp2_`): CC Cross Blur, CC Radial Blur, CC Vector Blur, Reduce Interlace Flicker,
|
||||
//! CC Composite, Color Link, Cineon Converter, HDR Compander, HDR Highlight Compression, Grow
|
||||
//! Bounds, CC Overbrights, CC Block Load, CC Burn Film, CC Glass, CC HexTile, CC Mr. Smoothie,
|
||||
//! CC Plastic, Inner/Outer Key, CC Simple Wire Removal and Basic 3D, with the CPU effects'
|
||||
//! exact steps (padding, box-blur radii, parameter conversions in f64 on the CPU).
|
||||
//!
|
||||
//! What only depends on parameters and geometry is prepared on the CPU and shared with the CPU
|
||||
//! effect: Inner/Outer Key's trimap and Cleanup strokes (`effects::inner_outer_plan`), CC Glass /
|
||||
//! Plastic's light (`effects::BumpLight`). Color Link's sample statistics (sorting, trimmed
|
||||
//! means) run on the CPU over the read-back layer (`effects::color_link_sample`) unless a Source
|
||||
//! Layer is chosen; the colouring runs here.
|
||||
|
||||
use effectcraft_color::BlendMode;
|
||||
use effectcraft_effects::EffectCtx;
|
||||
use effectcraft_raster::Image;
|
||||
|
||||
use crate::context::{Enc, GpuImage, Params};
|
||||
use crate::effects::{GBuf, box_passes, gaussian_blur};
|
||||
use crate::ops::mode_id;
|
||||
|
||||
/// Compute entry points in `fx_pixel2.wgsl`.
|
||||
pub(crate) const KERNELS: &[&str] = &[
|
||||
"fp2_cross",
|
||||
"fp2_box",
|
||||
"fp2_radial",
|
||||
"fp2_plane",
|
||||
"fp2_vector",
|
||||
"fp2_point",
|
||||
"fp2_block_means",
|
||||
"fp2_block_load",
|
||||
"fp2_burn",
|
||||
"fp2_bump",
|
||||
"fp2_hextile",
|
||||
"fp2_smoothie",
|
||||
"fp2_io_prep",
|
||||
"fp2_io_known",
|
||||
"fp2_io_est",
|
||||
"fp2_io_strokes",
|
||||
"fp2_io_final",
|
||||
"fp2_wire",
|
||||
"fp2_basic3d",
|
||||
];
|
||||
|
||||
/// Effect ids implemented here.
|
||||
pub(crate) const IDS: &[&str] = &[
|
||||
"ec.blur.cccross",
|
||||
"ec.blur.ccradial",
|
||||
"ec.blur.ccvector",
|
||||
"ec.blur.reduceflicker",
|
||||
"ec.channel.cccomposite",
|
||||
"ec.color.colorlink",
|
||||
"ec.utility.cineon",
|
||||
"ec.utility.hdrcompander",
|
||||
"ec.utility.hdrcompression",
|
||||
"ec.utility.growbounds",
|
||||
"ec.utility.ccoverbrights",
|
||||
"ec.stylize.ccblockload",
|
||||
"ec.stylize.ccburnfilm",
|
||||
"ec.stylize.ccglass",
|
||||
"ec.stylize.cchextile",
|
||||
"ec.stylize.ccmrsmoothie",
|
||||
"ec.stylize.ccplastic",
|
||||
"ec.key.innerouter",
|
||||
"ec.key.ccsimplewireremoval",
|
||||
"ec.obsolete.basic3d",
|
||||
];
|
||||
|
||||
/// Run effect `id` (one of [`IDS`]); `None` = this parameter combination runs on the CPU.
|
||||
pub(crate) fn apply(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
match id {
|
||||
"ec.blur.cccross" => cross_blur(e, ctx, b),
|
||||
"ec.blur.ccradial" => radial_blur(e, ctx, b),
|
||||
"ec.blur.ccvector" => vector_blur(e, ctx, b),
|
||||
"ec.blur.reduceflicker" => reduce_flicker(e, ctx, b),
|
||||
"ec.utility.growbounds" => grow_bounds(e, ctx, b),
|
||||
"ec.stylize.ccblockload" => block_load(e, ctx, b),
|
||||
"ec.stylize.ccburnfilm" => burn_film(e, ctx, b),
|
||||
"ec.stylize.ccglass" | "ec.stylize.ccplastic" => bump(e, id, ctx, b),
|
||||
"ec.stylize.cchextile" => hextile(e, ctx, b),
|
||||
"ec.stylize.ccmrsmoothie" => smoothie(e, ctx, b),
|
||||
"ec.key.innerouter" => inner_outer(e, ctx, b),
|
||||
"ec.key.ccsimplewireremoval" => wire_removal(e, ctx, b),
|
||||
"ec.obsolete.basic3d" => basic_3d(e, ctx, b),
|
||||
_ => point(e, id, ctx, b),
|
||||
}
|
||||
}
|
||||
|
||||
/// A same-size per-pixel kernel over the buffer.
|
||||
fn run(e: &mut Enc, entry: &str, p: &Params, mut b: GBuf, aux: Option<&GpuImage>, data: Option<&wgpu::Buffer>) -> Option<GBuf> {
|
||||
let out = e.scratch(b.img.width, b.img.height);
|
||||
e.pixels(entry, p, &b.img, aux, &out, data);
|
||||
b.img = out;
|
||||
Some(b)
|
||||
}
|
||||
|
||||
/// A one-value plane (x) of `img` (`fp2_plane` kind / property), softened by util::gauss_plane.
|
||||
fn plane(e: &mut Enc, img: &GpuImage, kind: u32, prop: u32, soft: f64) -> GpuImage {
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [kind, prop, 0, 0];
|
||||
let pl = e.scratch(img.width, img.height);
|
||||
e.pixels("fp2_plane", &p, img, None, &pl, None);
|
||||
gauss_plane(e, &pl, soft, soft)
|
||||
}
|
||||
|
||||
/// util::gauss_plane on every channel: 3 box passes per axis (edges repeated), each window
|
||||
/// summed directly (the CPU sums in f64; see `fp2_box`).
|
||||
pub(crate) fn gauss_plane(e: &mut Enc, img: &GpuImage, sx: f64, sy: f64) -> GpuImage {
|
||||
let mut cur = img.clone();
|
||||
for (vertical, sigma) in [(0, sx), (1, sy)] {
|
||||
if sigma <= 0.05 {
|
||||
continue;
|
||||
}
|
||||
for r in crate::effects::box_radii(sigma, 3) {
|
||||
if r == 0 {
|
||||
continue;
|
||||
}
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [r as u32, vertical, 0, 0];
|
||||
let out = e.scratch(cur.width, cur.height);
|
||||
e.pixels("fp2_box", &p, &cur, None, &out, None);
|
||||
cur = out;
|
||||
}
|
||||
}
|
||||
cur
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- blurs (blur2, blur3)
|
||||
|
||||
fn cross_blur(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
|
||||
let rx = (ctx.params.f("radiusX").max(0.0) * b.scale).round() as usize;
|
||||
let ry = (ctx.params.f("radiusY").max(0.0) * b.scale).round() as usize;
|
||||
if rx == 0 && ry == 0 {
|
||||
return Some(b);
|
||||
}
|
||||
if !ctx.adjustment {
|
||||
b.pad(e, (rx.max(ry) * 3) as u32 + 1)?;
|
||||
}
|
||||
let repeat = ctx.adjustment;
|
||||
let hz = box_passes(e, &b.img, &[rx; 3], &[], repeat);
|
||||
let vt = box_passes(e, &b.img, &[], &[ry; 3], repeat);
|
||||
let mut p = Params::default();
|
||||
p.u[0][0] = ctx.params.e("transferMode");
|
||||
let out = e.scratch(b.img.width, b.img.height);
|
||||
e.pixels("fp2_cross", &p, &hz, Some(&vt), &out, None);
|
||||
b.img = out;
|
||||
Some(b)
|
||||
}
|
||||
|
||||
fn radial_blur(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let amount = ctx.params.f("amount");
|
||||
if amount == 0.0 {
|
||||
return Some(b);
|
||||
}
|
||||
let c = b.to_px(ctx.params.v2("center"));
|
||||
let mut p = Params::default();
|
||||
p.u[0][0] = ctx.params.e("type");
|
||||
p.f[0] = [c.0 as f32, c.1 as f32, ((amount / 100.0).clamp(-1.0, 1.0) * 0.5) as f32, (amount.to_radians() * 0.25) as f32];
|
||||
p.f[1][0] = ctx.params.f("quality").clamp(1.0, 100.0) as f32;
|
||||
run(e, "fp2_radial", &p, b, None, None)
|
||||
}
|
||||
|
||||
fn vector_blur(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let amount = ctx.params.f("amount").max(0.0) * b.scale;
|
||||
if amount < 0.5 {
|
||||
return Some(b);
|
||||
}
|
||||
let soft = ctx.params.f("mapSoftness").max(0.0) * 0.5 * b.scale;
|
||||
let map = plane(e, &b.img, 0, 0, soft);
|
||||
let mut p = Params::default();
|
||||
p.u[0][0] = ctx.params.e("type");
|
||||
p.f[0] = [amount as f32, ctx.params.f("angleOffset").to_radians() as f32, 0.0, 0.0];
|
||||
run(e, "fp2_vector", &p, b, Some(&map), None)
|
||||
}
|
||||
|
||||
fn reduce_flicker(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
|
||||
let s = ctx.params.f("softness").max(0.0) * b.scale;
|
||||
if s < 0.05 {
|
||||
return Some(b);
|
||||
}
|
||||
b.img = gaussian_blur(e, &b.img, 0.0, s, true);
|
||||
Some(b)
|
||||
}
|
||||
|
||||
fn grow_bounds(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
|
||||
let px = (ctx.params.f("pixels") * b.scale).round().max(0.0) as u32;
|
||||
if !ctx.adjustment {
|
||||
b.pad(e, px)?;
|
||||
}
|
||||
Some(b)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- per-pixel colour effects
|
||||
|
||||
/// channel2::cc_composite's Composite Original modes.
|
||||
fn composite_mode(m: u32) -> BlendMode {
|
||||
match m {
|
||||
2 => BlendMode::Add,
|
||||
3 => BlendMode::Multiply,
|
||||
4 => BlendMode::Screen,
|
||||
5 => BlendMode::Overlay,
|
||||
6 => BlendMode::SoftLight,
|
||||
7 => BlendMode::HardLight,
|
||||
8 => BlendMode::Darken,
|
||||
9 => BlendMode::Lighten,
|
||||
10 => BlendMode::Difference,
|
||||
11 => BlendMode::Hue,
|
||||
12 => BlendMode::Saturation,
|
||||
13 => BlendMode::Color,
|
||||
14 => BlendMode::Luminosity,
|
||||
15 => BlendMode::StencilAlpha,
|
||||
_ => BlendMode::Normal,
|
||||
}
|
||||
}
|
||||
|
||||
fn point(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let pr = ctx.params;
|
||||
let mut p = Params::default();
|
||||
match id {
|
||||
"ec.utility.cineon" => {
|
||||
// utility::Cineon's offset and knee, in the CPU's f32.
|
||||
let black = pr.f("tenBitBlackPoint") as f32;
|
||||
let white = pr.f("tenBitWhitePoint") as f32;
|
||||
let rolloff = pr.f("highlightRolloff") as f32;
|
||||
let offset = |black: f32, white: f32| 10f32.powf((black - white) * 0.002 / 0.6);
|
||||
let knee = 1.0 - (rolloff / 100.0).clamp(0.0, 0.99);
|
||||
p.u[0] = [0, pr.e("conversionType"), 0, 0];
|
||||
p.f[0] = [offset(black, white), knee, white, pr.f("gamma") as f32];
|
||||
p.f[1] = [pr.f("internalBlackPoint") as f32, pr.f("internalWhitePoint") as f32, 0.0, 0.0];
|
||||
p.f[2] = [offset(95.0, 685.0), knee, 685.0, 1.7];
|
||||
}
|
||||
"ec.utility.hdrcompander" => {
|
||||
p.u[0] = [1, (pr.e("mode") == 1) as u32, 0, 0];
|
||||
p.f[0] = [pr.f("gain").max(1e-6) as f32, pr.f("gamma").max(1e-3) as f32, 0.0, 0.0];
|
||||
}
|
||||
"ec.utility.hdrcompression" => {
|
||||
let t = (pr.f("amount") / 100.0).clamp(0.0, 1.0) as f32;
|
||||
if t <= 0.0 {
|
||||
return Some(b);
|
||||
}
|
||||
p.u[0][0] = 2;
|
||||
p.f[0][0] = 1.0 - 0.5 * t;
|
||||
}
|
||||
"ec.utility.ccoverbrights" => {
|
||||
p.u[0] = [3, pr.e("channel"), 0, 0];
|
||||
p.f[0] = pr.color("highlightColor");
|
||||
}
|
||||
"ec.channel.cccomposite" => {
|
||||
let m = pr.e("compositeOriginal");
|
||||
let mode = if m == 1 { BlendMode::Normal } else { composite_mode(m) };
|
||||
p.u[0] = [6, (m == 1) as u32, mode_id(mode), pr.b("rgbOnly") as u32];
|
||||
p.f[0][0] = (pr.f("opacity") as f32 / 100.0).clamp(0.0, 1.0);
|
||||
}
|
||||
"ec.color.colorlink" => {
|
||||
// The sample statistics run on the CPU (the layer's own pixels are read back).
|
||||
let own = if ctx.layer_param("sourceLayer", false).is_none() { Some(e.download(&b.img)?) } else { None };
|
||||
let (c, sa) = effectcraft_effects::color_link_sample(ctx, own.as_ref())?;
|
||||
let op = (pr.f("opacity") / 100.0).clamp(0.0, 1.0) as f32;
|
||||
let stencil = pr.b("stencilOriginalAlpha") as u32;
|
||||
if pr.e("sampleSource") >= 6 {
|
||||
p.u[0] = [4, stencil, 0, 0];
|
||||
p.f[0] = [sa, op, 0.0, 0.0];
|
||||
} else {
|
||||
p.u[0] = [5, stencil, mode_id(effectcraft_effects::color_link_mode(ctx)), 0];
|
||||
p.f[0] = [c[0] * op, c[1] * op, c[2] * op, op];
|
||||
}
|
||||
}
|
||||
_ => return None,
|
||||
}
|
||||
run(e, "fp2_point", &p, b, None, None)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Block Load (stylize3)
|
||||
|
||||
fn block_load(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
|
||||
let c = (ctx.params.f("completion") / 100.0).clamp(0.0, 1.0);
|
||||
if c >= 1.0 {
|
||||
return Some(b);
|
||||
}
|
||||
let (w, h) = (b.img.width as usize, b.img.height as usize);
|
||||
let nlev = (w.max(h).max(2) as f64).log2().ceil() as u32;
|
||||
let lp = c * (nlev + 1) as f64;
|
||||
let done = lp.floor() as u32;
|
||||
let frac = (lp - done as f64) as f32;
|
||||
let size = |k: u32| 1usize << (nlev - k.min(nlev));
|
||||
let means = |e: &mut Enc, s: usize| {
|
||||
let mut p = Params::default();
|
||||
p.u[0][0] = s as u32;
|
||||
let g = e.scratch(w.div_ceil(s).max(1) as u32, h.div_ceil(s).max(1) as u32);
|
||||
e.pixels("fp2_block_means", &p, &b.img, None, &g, None);
|
||||
g
|
||||
};
|
||||
let s_new = size(done);
|
||||
let new = means(e, s_new);
|
||||
let old = (done > 0).then(|| (size(done - 1), means(e, size(done - 1))));
|
||||
let flags = ctx.params.b("scanlines") as u32
|
||||
| (ctx.params.b("smoothing") as u32) << 1
|
||||
| (ctx.params.b("bilinear") as u32) << 2
|
||||
| (old.is_some() as u32) << 3
|
||||
| (ctx.params.b("startCleared") as u32) << 4;
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [s_new as u32, old.as_ref().map_or(1, |o| o.0 as u32), flags, 0];
|
||||
p.f[0] = [frac, h.div_ceil(s_new) as f32, 0.0, 0.0];
|
||||
let out = e.scratch(w as u32, h as u32);
|
||||
e.pixels("fp2_block_load", &p, &new, old.as_ref().map(|o| &o.1), &out, None);
|
||||
b.img = out;
|
||||
Some(b)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Burn Film (stylize3)
|
||||
|
||||
fn burn_film(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let burn = (ctx.params.f("burn") / 100.0).clamp(0.0, 1.0) as f32;
|
||||
if burn <= 0.0 {
|
||||
return Some(b);
|
||||
}
|
||||
let c = b.to_px(ctx.params.v2("center"));
|
||||
let seed = ctx.params.f("randomSeed").max(0.0) as u32;
|
||||
let s = b.scale;
|
||||
let diag = (ctx.layer_size[0].hypot(ctx.layer_size[1]) * s).max(1.0);
|
||||
let feat = (60.0 * s).max(1e-3);
|
||||
let mut p = Params::default();
|
||||
p.u[0][0] = seed.wrapping_add(17);
|
||||
p.f[0] = [c.0 as f32, c.1 as f32, diag as f32, feat as f32];
|
||||
p.f[1] = [1.0 - burn * 1.25, 0.12 * (burn * 10.0).min(1.0), 0.0, 0.0];
|
||||
run(e, "fp2_burn", &p, b, None, None)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Glass / CC Plastic (stylize3)
|
||||
|
||||
fn bump(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let glass = id == "ec.stylize.ccglass";
|
||||
// stylize3::height_field.
|
||||
let src = crate::fx_key::fitted(e, ctx, &b, effectcraft_effects::bump_layer_id(ctx), true, false)?.unwrap_or_else(|| b.img.clone());
|
||||
let soft = ctx.params.f("softness") * b.scale * 0.5;
|
||||
let mut hf = plane(e, &src, 1, ctx.params.e("property"), soft);
|
||||
let height = ctx.params.f("height") as f32 / 100.0;
|
||||
let mut disp = 0.0;
|
||||
if glass {
|
||||
disp = (ctx.params.f("displacement") * b.scale * 0.25) as f32;
|
||||
} else {
|
||||
let lo = (ctx.params.f("cutMin") / 100.0) as f32;
|
||||
let hi = (ctx.params.f("cutMax") / 100.0) as f32;
|
||||
if lo > 0.0 || hi < 1.0 {
|
||||
hf = clamp_plane(e, &hf, lo.min(hi), hi.max(lo));
|
||||
}
|
||||
}
|
||||
let l = effectcraft_effects::BumpLight::from(ctx, &geo(&b));
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [glass as u32, l.point as u32, 0, 0];
|
||||
p.f[0] = [height * 25.0, disp, (height != 0.0) as u32 as f32, l.height];
|
||||
p.f[1] = [l.intensity, l.ambient, l.diffuse, l.specular];
|
||||
p.f[2] = [l.shininess, l.metal, l.pos.0 as f32, l.pos.1 as f32];
|
||||
p.f[3] = [l.color[0], l.color[1], l.color[2], 0.0];
|
||||
p.f[4] = [l.dir[0], l.dir[1], l.dir[2], 0.0];
|
||||
run(e, "fp2_bump", &p, b, Some(&hf), None)
|
||||
}
|
||||
|
||||
/// An empty CPU buffer with `b`'s geometry (for CPU helpers that only read positions).
|
||||
fn geo(b: &GBuf) -> effectcraft_effects::Buf {
|
||||
effectcraft_effects::Buf { img: Image { width: b.img.width, height: b.img.height, data: vec![] }, offset: b.offset, scale: b.scale }
|
||||
}
|
||||
|
||||
/// Clamp a plane (x) into [lo, hi] (Plane::map with f32::clamp).
|
||||
fn clamp_plane(e: &mut Enc, pl: &GpuImage, lo: f32, hi: f32) -> GpuImage {
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [3, 0, 0, 0];
|
||||
p.f[0] = [lo, hi, 0.0, 0.0];
|
||||
let out = e.scratch(pl.width, pl.height);
|
||||
e.pixels("fp2_plane", &p, pl, None, &out, None);
|
||||
out
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC HexTile / CC Mr. Smoothie (stylize3)
|
||||
|
||||
fn hextile(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let r = (ctx.params.f("radius") * b.scale).max(0.5);
|
||||
let c = b.to_px(ctx.params.v2("center"));
|
||||
let (sn, cs) = ctx.params.f("rotate").to_radians().sin_cos();
|
||||
let mut p = Params::default();
|
||||
p.u[0][0] = ctx.params.e("render");
|
||||
p.f[0] = [c.0 as f32, c.1 as f32, r as f32, (ctx.params.f("smearing") / 100.0).clamp(0.0, 1.0) as f32];
|
||||
p.f[1] = [sn as f32, cs as f32, 0.0, 0.0];
|
||||
run(e, "fp2_hextile", &p, b, None, None)
|
||||
}
|
||||
|
||||
fn smoothie(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let flow = crate::fx_key::fitted(e, ctx, &b, "flowLayer", true, false)?.unwrap_or_else(|| b.img.clone());
|
||||
let sm = ctx.params.f("smoothness") * b.scale * 0.5;
|
||||
let pl = plane(e, &flow, 2, ctx.params.e("property"), sm);
|
||||
let a = b.to_px(ctx.params.v2("sampleA"));
|
||||
let bb = b.to_px(ctx.params.v2("sampleB"));
|
||||
let mut p = Params::default();
|
||||
p.f[0] = [a.0 as f32, a.1 as f32, bb.0 as f32, bb.1 as f32];
|
||||
p.f[1] = [(ctx.params.f("phase") / 360.0) as f32, (ctx.params.e("colorLoop") + 1) as f32, 0.0, 0.0];
|
||||
run(e, "fp2_smoothie", &p, b, Some(&pl), None)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Inner/Outer Key (keying2)
|
||||
|
||||
fn inner_outer(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let Some(plan) = effectcraft_effects::inner_outer_plan(ctx, &geo(&b)) else { return Some(b) };
|
||||
let (w, h) = (b.img.width, b.img.height);
|
||||
let mut tri = Image::new(w, h);
|
||||
for (px, &t) in tri.data.iter_mut().zip(&plan.tri.data) {
|
||||
px[0] = t;
|
||||
}
|
||||
let tri = e.g.upload_image(&tri)?;
|
||||
// The matte (x, or w of the estimate) and the estimate's colours.
|
||||
let (mut matte, est) = if plan.unknown {
|
||||
let t = e.scratch(w, h);
|
||||
e.pixels("fp2_io_prep", &Params::default(), &b.img, Some(&tri), &t, None);
|
||||
let known = |e: &mut Enc, side: u32| {
|
||||
let mut p = Params::default();
|
||||
p.u[0][0] = side;
|
||||
let k = e.scratch(w, h);
|
||||
e.pixels("fp2_io_known", &p, &t, None, &k, None);
|
||||
gauss_plane(e, &k, plan.sigma, plan.sigma)
|
||||
};
|
||||
let fg = known(e, 0);
|
||||
let bg = known(e, 1);
|
||||
let (rows, stride) = e.image_rows(&t);
|
||||
let mut p = Params::default();
|
||||
p.u[0][0] = stride;
|
||||
let est = e.scratch(w, h);
|
||||
e.pixels("fp2_io_est", &p, &fg, Some(&bg), &est, Some(&rows));
|
||||
(est.clone(), Some(est))
|
||||
} else {
|
||||
(tri, None)
|
||||
};
|
||||
let from_w = est.is_some() as u32;
|
||||
if !plan.strokes.is_empty() || from_w != 0 {
|
||||
let n = plan.strokes.len();
|
||||
let mut data = Vec::with_capacity(n + n * (w * h) as usize);
|
||||
data.extend(plan.strokes.iter().map(|s| s.0));
|
||||
for (_, cov) in &plan.strokes {
|
||||
data.extend_from_slice(cov);
|
||||
}
|
||||
let buf = e.data(&data);
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [n as u32, from_w, 0, 0];
|
||||
let out = e.scratch(w, h);
|
||||
e.pixels("fp2_io_strokes", &p, &matte, None, &out, Some(&buf));
|
||||
matte = out;
|
||||
}
|
||||
if plan.thin.abs() > 0.01 {
|
||||
let modes = if plan.thin < 0.0 { crate::fx_color::DILATE_X } else { crate::fx_color::ERODE_X };
|
||||
matte = crate::fx_color::morph_frac(e, &matte, plan.thin.abs(), modes);
|
||||
}
|
||||
if plan.feather > 0.05 {
|
||||
matte = gauss_plane(e, &matte, plan.feather * 0.5, plan.feather * 0.5);
|
||||
}
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [0, from_w, plan.invert as u32, 0];
|
||||
p.f[0] = [plan.threshold, plan.original, 0.0, 0.0];
|
||||
match est {
|
||||
Some(est) => {
|
||||
let (rows, stride) = e.image_rows(&est);
|
||||
p.u[0][0] = stride;
|
||||
run(e, "fp2_io_final", &p, b, Some(&matte), Some(&rows))
|
||||
}
|
||||
None => run(e, "fp2_io_final", &p, b, Some(&matte), None),
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Simple Wire Removal (keying2)
|
||||
|
||||
fn wire_removal(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let half = (ctx.params.f("thickness") * b.scale * 0.5).max(0.0);
|
||||
if half <= 0.0 {
|
||||
return Some(b);
|
||||
}
|
||||
let slope = (ctx.params.f("slope") / 100.0).clamp(0.0, 1.0) as f32;
|
||||
let mirror = (ctx.params.f("mirrorBlend") / 100.0).clamp(0.0, 1.0) as f32;
|
||||
let a = b.to_px(ctx.params.v2("pointA"));
|
||||
let z = b.to_px(ctx.params.v2("pointB"));
|
||||
let (dx, dy) = (z.0 - a.0, z.1 - a.1);
|
||||
let len = (dx * dx + dy * dy).sqrt();
|
||||
if len < 1e-6 {
|
||||
return Some(b);
|
||||
}
|
||||
let (tx, ty) = (dx / len, dy / len);
|
||||
let mut p = Params::default();
|
||||
p.u[0][0] = ctx.params.e("removalStyle");
|
||||
p.f[0] = [a.0 as f32, a.1 as f32, -ty as f32, tx as f32];
|
||||
p.f[1] = [half as f32, (half * slope as f64) as f32, mirror, 0.0];
|
||||
run(e, "fp2_wire", &p, b, None, None)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Basic 3D (obsolete)
|
||||
|
||||
fn basic_3d(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let swivel = ctx.params.f("swivel").to_radians();
|
||||
let tilt = ctx.params.f("tilt").to_radians();
|
||||
let dist = ctx.params.f("distanceToImage") * b.scale;
|
||||
let spec_on = ctx.params.b("specularHighlight");
|
||||
let wire = ctx.params.b("preview/drawPreviewWireframe");
|
||||
if swivel == 0.0 && tilt == 0.0 && dist == 0.0 && !spec_on && !wire {
|
||||
return Some(b);
|
||||
}
|
||||
let (lx, ly, lw, lh) = (b.offset[0], b.offset[1], ctx.layer_size[0] * b.scale, ctx.layer_size[1] * b.scale);
|
||||
let (cx, cy) = (lx + lw * 0.5, ly + lh * 0.5);
|
||||
let f = lw.max(lh).max(1.0) * 1.4;
|
||||
let (cs, ss) = (swivel.cos(), swivel.sin());
|
||||
let (ct, st) = (tilt.cos(), tilt.sin());
|
||||
let v3 = |v: [f64; 3]| [v[0] as f32, v[1] as f32, v[2] as f32, 0.0];
|
||||
let l = [-0.5f64, -0.6, -1.0];
|
||||
let k = 1.0 / (l[0] * l[0] + l[1] * l[1] + l[2] * l[2]).sqrt();
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [spec_on as u32, wire as u32, 0, 0];
|
||||
p.f[0] = [cx as f32, cy as f32, f as f32, dist as f32];
|
||||
p.f[1] = v3([cs, 0.0, -ss]);
|
||||
p.f[2] = v3([ss * st, ct, cs * st]);
|
||||
p.f[3] = v3([ss * ct, -st, cs * ct]);
|
||||
p.f[4] = v3(l.map(|v| v * k));
|
||||
p.f[5] = [(lw * 0.5) as f32, (lh * 0.5) as f32, 0.0, 0.0];
|
||||
run(e, "fp2_basic3d", &p, b, None, None)
|
||||
}
|
||||
|
|
@ -22,14 +22,16 @@
|
|||
//! GPU effects ([`effectcraft_effects::GPU_EFFECTS`]) run as compute kernels with the CPU
|
||||
//! effect's exact steps (padding, box-blur radii, parameter conversions); chains of them are
|
||||
//! uploaded and read back once. Each family lives in its own module with its own WGSL file
|
||||
//! (`fx_color`, `fx_depth`, `fx_distort`, `fx_extra`, `fx_generate`, `fx_key`, `fx_light`,
|
||||
//! `fx_lut`, `fx_noise`, `fx_particles`, `fx_sim`, `fx_stylize`, `fx_text`, `fx_time`, `fx_tone`,
|
||||
//! `fx_transition`, `fx_vr`, `fx_warp`); settings a kernel cannot match fall back to the CPU
|
||||
//! (`catalog::gpu_supported`, or `None` from the family's `apply`).
|
||||
//! (`fx_color`, `fx_depth`, `fx_distort`, `fx_extra`, `fx_gen2`, `fx_generate`, `fx_key`,
|
||||
//! `fx_light`, `fx_lut`, `fx_noise`, `fx_particles`, `fx_pixel2`, `fx_sim`, `fx_stylize`,
|
||||
//! `fx_text`, `fx_time`, `fx_tone`, `fx_transition`, `fx_vr`, `fx_warp`); settings a kernel
|
||||
//! cannot match fall back to the CPU (`catalog::gpu_supported`, or `None` from the family's
|
||||
//! `apply`).
|
||||
//! The 3D Channel effects read the layer's aux channels as an extra texture (`fx_depth`), the
|
||||
//! colour-management effects interpret colour programs built next to the CPU effects
|
||||
//! (`fx_lut`), and simulations keep their state on the CPU and rasterise its per-frame plan here
|
||||
//! (`fx_sim`, `fx_particles`).
|
||||
//! (`fx_lut`), simulations keep their state on the CPU and rasterise its per-frame plan here
|
||||
//! (`fx_sim`, `fx_particles`), and so do the geometry generators (`fx_gen2`: bolts, strokes,
|
||||
//! glyphs, audio marks, waves).
|
||||
//!
|
||||
//! Working textures come from a pool (`context::Pool`): a frame allocates several full-frame
|
||||
//! RGBA f32 images per layer, and creating and zeroing those cost more than compositing a small
|
||||
|
|
@ -59,12 +61,14 @@ mod fx_color;
|
|||
mod fx_depth;
|
||||
mod fx_distort;
|
||||
mod fx_extra;
|
||||
mod fx_gen2;
|
||||
mod fx_generate;
|
||||
mod fx_key;
|
||||
mod fx_light;
|
||||
mod fx_lut;
|
||||
mod fx_noise;
|
||||
mod fx_particles;
|
||||
mod fx_pixel2;
|
||||
mod fx_sim;
|
||||
mod fx_stylize;
|
||||
mod fx_text;
|
||||
|
|
@ -323,6 +327,8 @@ mod tests_fx_distort;
|
|||
#[cfg(test)]
|
||||
mod tests_fx_extra;
|
||||
#[cfg(test)]
|
||||
mod tests_fx_gen2;
|
||||
#[cfg(test)]
|
||||
mod tests_fx_generate;
|
||||
#[cfg(test)]
|
||||
mod tests_fx_key;
|
||||
|
|
@ -335,6 +341,8 @@ mod tests_fx_noise;
|
|||
#[cfg(test)]
|
||||
mod tests_fx_particles;
|
||||
#[cfg(test)]
|
||||
mod tests_fx_pixel2;
|
||||
#[cfg(test)]
|
||||
mod tests_fx_sim;
|
||||
#[cfg(test)]
|
||||
mod tests_fx_stylize;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,805 @@
|
|||
// GPU effects, generators of part C: see src/fx_gen2.rs. Every entry point is prefixed `fg2_`.
|
||||
// Geometry computed on the CPU (bolts, strokes, glyphs, audio marks, waves) arrives as item
|
||||
// tables in `data` — tile offsets, item indices, items (as fx_sim's tiled raster) — and is
|
||||
// rasterised here with the CPU's coverage functions; closed-form generators run per pixel.
|
||||
|
||||
fn fg2_inside(p: vec2<i32>) -> bool {
|
||||
let d = out_dims();
|
||||
return p.x < d.x && p.y < d.y;
|
||||
}
|
||||
|
||||
// util::unpremul (colour 0 below alpha 1e-6).
|
||||
fn fg2_unpremul(p: vec4<f32>) -> vec3<f32> {
|
||||
if (p.w > 1e-6) {
|
||||
return p.xyz / p.w;
|
||||
}
|
||||
return vec3<f32>(0.0);
|
||||
}
|
||||
|
||||
// util::smoothstep.
|
||||
fn fg2_smoothstep(e0: f32, e1: f32, x: f32) -> f32 {
|
||||
if (abs(e1 - e0) < 1e-9) {
|
||||
return select(1.0, 0.0, x < e0);
|
||||
}
|
||||
let t = clamp((x - e0) / (e1 - e0), 0.0, 1.0);
|
||||
return t * t * (3.0 - 2.0 * t);
|
||||
}
|
||||
|
||||
// Premultiplied "over": `s` on top of `d`.
|
||||
fn fg2_over(d: vec4<f32>, s: vec4<f32>) -> vec4<f32> {
|
||||
return s + d * (1.0 - s.w);
|
||||
}
|
||||
|
||||
// The tile's item range: (first index slot, end slot).
|
||||
fn fg2_tile(p: vec2<i32>, tiles_x: u32) -> vec2<u32> {
|
||||
let tile = u32(p.y / 16) * tiles_x + u32(p.x / 16);
|
||||
return vec2<u32>(u32(data[tile]), u32(data[tile + 1u]));
|
||||
}
|
||||
|
||||
// Whether pixel `p` lies in the integer bounds stored at the end of item `at` (stride `stride`).
|
||||
fn fg2_in_bounds(p: vec2<i32>, at: u32, stride: u32) -> bool {
|
||||
let bb = at + stride - 4u;
|
||||
return !(f32(p.x) < data[bb] || f32(p.y) < data[bb + 1u] || f32(p.x) > data[bb + 2u] || f32(p.y) > data[bb + 3u]);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- segments (generate3::raster_segs)
|
||||
// Items: (ax, ay, bx, by, radius, value, bounds). Up to four coverages per pixel (x, y, z, w),
|
||||
// variant k with radius max(r · f[k].x + f[k].y, f[k].z) and hardness f[k].w, max-combined;
|
||||
// bit k of u[1].x selects Advanced Lightning's linear core (clamp(r − d + 0.5, 0, 1)) instead.
|
||||
// u[0] = (items at, stride, tiles x, variants).
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fg2_segs(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fg2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let stride = P.u[0].y;
|
||||
let nv = P.u[0].w;
|
||||
let rg = fg2_tile(p, P.u[0].z);
|
||||
let px = f32(p.x) + 0.5;
|
||||
let py = f32(p.y) + 0.5;
|
||||
var o = vec4<f32>(0.0);
|
||||
for (var k = rg.x; k < rg.y; k++) {
|
||||
let at = P.u[0].x + u32(data[k]) * stride;
|
||||
if (!fg2_in_bounds(p, at, stride)) {
|
||||
continue;
|
||||
}
|
||||
let ax = data[at];
|
||||
let ay = data[at + 1u];
|
||||
let dx = data[at + 2u] - ax;
|
||||
let dy = data[at + 3u] - ay;
|
||||
let r = data[at + 4u];
|
||||
let v = data[at + 5u];
|
||||
let l2 = dx * dx + dy * dy;
|
||||
var t = 0.0;
|
||||
if (l2 > 1e-12) {
|
||||
t = clamp(((px - ax) * dx + (py - ay) * dy) / l2, 0.0, 1.0);
|
||||
}
|
||||
let ex = px - ax - dx * t;
|
||||
let ey = py - ay - dy * t;
|
||||
let d = sqrt(ex * ex + ey * ey);
|
||||
for (var j = 0u; j < nv; j++) {
|
||||
let vf = P.f[j];
|
||||
let rk = max(r * vf.x + vf.y, vf.z);
|
||||
var c = 0.0;
|
||||
if (((P.u[1].x >> j) & 1u) != 0u) {
|
||||
c = clamp(rk - d + 0.5, 0.0, 1.0) * v;
|
||||
} else {
|
||||
let inner = rk * clamp(vf.w, 0.0, 1.0) - 0.5;
|
||||
let outer = rk + 0.5;
|
||||
if (d >= outer) {
|
||||
continue;
|
||||
}
|
||||
if (d <= inner) {
|
||||
c = v;
|
||||
} else {
|
||||
c = (1.0 - fg2_smoothstep(inner, outer, d)) * v;
|
||||
}
|
||||
}
|
||||
if (c > o[j]) {
|
||||
o[j] = c;
|
||||
}
|
||||
}
|
||||
}
|
||||
textureStore(out, p, o);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- convex polygons (generate3::raster_convex)
|
||||
// Items: (vertex count, 16 vertex pairs, bounds); coverage = union, 4 × 4 samples per pixel.
|
||||
// u[0] = (items at, stride, tiles x).
|
||||
|
||||
fn fg2_in_poly(at: u32, x: f32, y: f32) -> bool {
|
||||
let n = u32(data[at]);
|
||||
var sgn = 0.0;
|
||||
for (var i = 0u; i < n; i++) {
|
||||
let j = (i + 1u) % n;
|
||||
let ax = data[at + 1u + 2u * i];
|
||||
let ay = data[at + 2u + 2u * i];
|
||||
let bx = data[at + 1u + 2u * j];
|
||||
let by = data[at + 2u + 2u * j];
|
||||
let c = (bx - ax) * (y - ay) - (by - ay) * (x - ax);
|
||||
if (abs(c) < 1e-12) {
|
||||
continue;
|
||||
}
|
||||
if (sgn == 0.0) {
|
||||
sgn = sign(c);
|
||||
} else if (sign(c) != sgn) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fg2_convex(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fg2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let stride = P.u[0].y;
|
||||
let rg = fg2_tile(p, P.u[0].z);
|
||||
var o = 0.0;
|
||||
for (var k = rg.x; k < rg.y; k++) {
|
||||
let at = P.u[0].x + u32(data[k]) * stride;
|
||||
if (!fg2_in_bounds(p, at, stride)) {
|
||||
continue;
|
||||
}
|
||||
var hit = 0;
|
||||
for (var sy = 0; sy < 4; sy++) {
|
||||
for (var sx = 0; sx < 4; sx++) {
|
||||
if (fg2_in_poly(at, f32(p.x) + (f32(sx) + 0.5) / 4.0, f32(p.y) + (f32(sy) + 0.5) / 4.0)) {
|
||||
hit += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
o = max(o, f32(hit) / 16.0);
|
||||
}
|
||||
textureStore(out, p, vec4<f32>(o, 0.0, 0.0, 0.0));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- painting
|
||||
// generate3::paint (style 0 on the layer, 1 on transparency, 2 reveal) and Vegas' Composite
|
||||
// Under (3). aux = coverage (x); u[0].x = style; f[0] = colour; f[1].x = opacity.
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fg2_paint(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fg2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let px = textureLoad(src, p, 0);
|
||||
let c = textureLoad(aux, p, 0).x;
|
||||
let col = P.f[0];
|
||||
var o: vec4<f32>;
|
||||
if (P.u[0].x == 3u) {
|
||||
let a = c * col.w;
|
||||
o = fg2_over(vec4<f32>(col.xyz * a, a), px);
|
||||
} else {
|
||||
let a = clamp(c * P.f[1].x * col.w, 0.0, 1.0);
|
||||
let s = vec4<f32>(col.xyz * a, a);
|
||||
switch P.u[0].x {
|
||||
case 1u: {
|
||||
o = s;
|
||||
}
|
||||
case 2u: {
|
||||
o = px * a;
|
||||
}
|
||||
default: {
|
||||
o = fg2_over(px, s);
|
||||
}
|
||||
}
|
||||
}
|
||||
textureStore(out, p, o);
|
||||
}
|
||||
|
||||
// generate3::draw_marks: aux = (core, halo); u[0].x = composite; f[0] = inside, f[1] = outside.
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fg2_marks(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fg2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let cv = textureLoad(aux, p, 0);
|
||||
let c = cv.x;
|
||||
let a = max(c, cv.y);
|
||||
var base = vec4<f32>(0.0);
|
||||
if (P.u[0].x != 0u) {
|
||||
base = textureLoad(src, p, 0);
|
||||
}
|
||||
if (a <= 0.0) {
|
||||
textureStore(out, p, base);
|
||||
return;
|
||||
}
|
||||
let t = c / a;
|
||||
let col = P.f[1].xyz + (P.f[0].xyz - P.f[1].xyz) * t;
|
||||
textureStore(out, p, fg2_over(base, vec4<f32>(col * a, a)));
|
||||
}
|
||||
|
||||
// (src.x, aux.x) into (x, y), src.y into z: a coverage next to its blurred copy.
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fg2_pair(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fg2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let s = textureLoad(src, p, 0);
|
||||
textureStore(out, p, vec4<f32>(s.x, textureLoad(aux, p, 0).x, s.y, 0.0));
|
||||
}
|
||||
|
||||
// textfx::lightning's composite: aux = (outer, glow, core); u[0].x = blending mode;
|
||||
// f[0] = outside colour, f[1] = inside colour.
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fg2_bolt(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fg2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let px = textureLoad(src, p, 0);
|
||||
let m = textureLoad(aux, p, 0);
|
||||
let o = max(m.x, m.y * 0.8);
|
||||
let c = m.z;
|
||||
let a = min(max(o, c), 1.0);
|
||||
if (a <= 0.0) {
|
||||
textureStore(out, p, px);
|
||||
return;
|
||||
}
|
||||
let t = c / a;
|
||||
let oc = P.f[0].xyz;
|
||||
let colr = oc + (P.f[1].xyz - oc) * t;
|
||||
var r: vec4<f32>;
|
||||
switch P.u[0].x {
|
||||
case 1u: {
|
||||
r = vec4<f32>(px.xyz + colr * a, min(px.w + a * (1.0 - px.w), 1.0));
|
||||
}
|
||||
case 2u: {
|
||||
let s = vec4<f32>(colr * a, a);
|
||||
r = px + s - px * s;
|
||||
}
|
||||
default: {
|
||||
r = fg2_over(px, vec4<f32>(colr * a, a));
|
||||
}
|
||||
}
|
||||
textureStore(out, p, r);
|
||||
}
|
||||
|
||||
// generate2::advanced_lightning's composite: aux = (core, blurred core); u[0] = (composite, glow);
|
||||
// f[0] = (core opacity, glow opacity); f[1] = core colour, f[2] = glow colour.
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fg2_adv_bolt(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fg2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let px = textureLoad(src, p, 0);
|
||||
let m = textureLoad(aux, p, 0);
|
||||
let cv = m.x * P.f[0].x;
|
||||
var g = 0.0;
|
||||
if (P.u[0].y != 0u) {
|
||||
g = min(m.y * 3.0, 1.0);
|
||||
}
|
||||
let gv = g * P.f[0].y;
|
||||
let l = P.f[1].xyz * cv + P.f[2].xyz * gv;
|
||||
let la = min(max(cv, gv), 1.0);
|
||||
if (P.u[0].x != 0u) {
|
||||
let k = clamp(la, 0.0, 1.0);
|
||||
textureStore(out, p, vec4<f32>(px.xyz + l, clamp(px.w + k * (1.0 - px.w), 0.0, 1.0)));
|
||||
} else {
|
||||
textureStore(out, p, vec4<f32>(l, la));
|
||||
}
|
||||
}
|
||||
|
||||
// textfx::text_px over the previous pass: aux = (fill, outer, inner) coverages.
|
||||
// u[0] = (display, keep the layer, stroked, inner ring); f[0] = fill, f[1] = stroke colour;
|
||||
// f[2].x = opacity.
|
||||
fn fg2_tint(c: vec4<f32>, a0: f32) -> vec4<f32> {
|
||||
let a = clamp(a0 * c.w, 0.0, 1.0);
|
||||
return vec4<f32>(c.xyz * a, a);
|
||||
}
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fg2_text(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fg2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let cov = textureLoad(aux, p, 0);
|
||||
let op = P.f[2].x;
|
||||
let f = cov.x * op;
|
||||
var ring = 0.0;
|
||||
if (P.u[0].z != 0u) {
|
||||
var inner = 0.0;
|
||||
if (P.u[0].w != 0u) {
|
||||
inner = cov.z;
|
||||
}
|
||||
ring = max(cov.y - inner, 0.0);
|
||||
}
|
||||
let s = ring * op;
|
||||
var o = vec4<f32>(0.0);
|
||||
if (P.u[0].y != 0u) {
|
||||
o = textureLoad(src, p, 0);
|
||||
}
|
||||
let fill = fg2_tint(P.f[0], f);
|
||||
let stroke = fg2_tint(P.f[1], s);
|
||||
switch P.u[0].x {
|
||||
case 1u: {
|
||||
o = fg2_over(o, stroke);
|
||||
}
|
||||
case 2u: {
|
||||
o = fg2_over(fg2_over(o, stroke), fill);
|
||||
}
|
||||
case 3u: {
|
||||
o = fg2_over(fg2_over(o, fill), stroke);
|
||||
}
|
||||
default: {
|
||||
o = fg2_over(o, fill);
|
||||
}
|
||||
}
|
||||
textureStore(out, p, o);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Radio Waves (generate2::radio_waves)
|
||||
// data: waves (stride 13: cx, cy, radius, half width, rotation, fade, colour rgba, opacity,
|
||||
// outline offset, outline vertices), then outline vertices (x, y relative to the centre).
|
||||
// aux = the contour's signed distance (x) when u[0].y. u[0] = (waves, contour, profile);
|
||||
// f[0] = (anti-aliasing, anchor x, anchor y).
|
||||
|
||||
fn fg2_rw_profile(kind: u32, u0: f32) -> f32 {
|
||||
let u = clamp(u0, -1.0, 1.0);
|
||||
switch kind {
|
||||
case 1u: {
|
||||
return (1.0 - u) * 0.5;
|
||||
}
|
||||
case 2u: {
|
||||
return (1.0 + u) * 0.5;
|
||||
}
|
||||
case 3u: {
|
||||
return 1.0 - abs(u);
|
||||
}
|
||||
case 4u: {
|
||||
return cos(u * 1.5707963267948966);
|
||||
}
|
||||
case 5u: {
|
||||
if (u < 0.0) {
|
||||
return 1.0 + u;
|
||||
}
|
||||
return 1.0;
|
||||
}
|
||||
case 6u: {
|
||||
if (u > 0.0) {
|
||||
return 1.0 - u;
|
||||
}
|
||||
return 1.0;
|
||||
}
|
||||
default: {
|
||||
return 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// generate2::outline_dist.
|
||||
fn fg2_outline_dist(off: u32, n: u32, dx: f32, dy: f32) -> f32 {
|
||||
let r = sqrt(dx * dx + dy * dy);
|
||||
if (r < 1e-9) {
|
||||
var m = 3.0e38;
|
||||
for (var k = 0u; k < n; k++) {
|
||||
let x = data[off + 2u * k];
|
||||
let y = data[off + 2u * k + 1u];
|
||||
m = min(m, sqrt(x * x + y * y));
|
||||
}
|
||||
return m;
|
||||
}
|
||||
let dir = vec2<f32>(dx / r, dy / r);
|
||||
var best = 3.0e38;
|
||||
for (var k = 0u; k < n; k++) {
|
||||
let j = (k + 1u) % n;
|
||||
let p0 = vec2<f32>(data[off + 2u * k], data[off + 2u * k + 1u]);
|
||||
let p1 = vec2<f32>(data[off + 2u * j], data[off + 2u * j + 1u]);
|
||||
let e = p1 - p0;
|
||||
let den = dir.x * e.y - dir.y * e.x;
|
||||
if (abs(den) < 1e-12) {
|
||||
continue;
|
||||
}
|
||||
let t = (p0.x * e.y - p0.y * e.x) / den;
|
||||
let q = vec2<f32>(dir.x * t - p0.x, dir.y * t - p0.y);
|
||||
let el2 = max(e.x * e.x + e.y * e.y, 1e-12);
|
||||
let s = (q.x * e.x + q.y * e.y) / el2;
|
||||
if (t <= 0.0 || s < -1e-5 || s > 1.0 + 1e-5) {
|
||||
continue;
|
||||
}
|
||||
best = min(best, abs(r - t) * abs(den) / sqrt(el2));
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
// util::point_in_poly (even-odd).
|
||||
fn fg2_point_in_poly(off: u32, n: u32, x: f32, y: f32) -> bool {
|
||||
if (n < 3u) {
|
||||
return false;
|
||||
}
|
||||
var inside = false;
|
||||
var j = n - 1u;
|
||||
for (var i = 0u; i < n; i++) {
|
||||
let ax = data[off + 2u * i];
|
||||
let ay = data[off + 2u * i + 1u];
|
||||
let bx = data[off + 2u * j];
|
||||
let by = data[off + 2u * j + 1u];
|
||||
if ((ay > y) != (by > y) && x < (bx - ax) * (y - ay) / (by - ay) + ax) {
|
||||
inside = !inside;
|
||||
}
|
||||
j = i;
|
||||
}
|
||||
return inside;
|
||||
}
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fg2_radio(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fg2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
var o = textureLoad(src, p, 0);
|
||||
let aa = P.f[0].x;
|
||||
for (var w = 0u; w < P.u[0].x; w++) {
|
||||
let at = w * 13u;
|
||||
let dx = f32(p.x) + 0.5 - data[at];
|
||||
let dy = f32(p.y) + 0.5 - data[at + 1u];
|
||||
let radius = data[at + 2u];
|
||||
let hw = data[at + 3u];
|
||||
let n = u32(data[at + 12u]);
|
||||
var d: f32;
|
||||
if (P.u[0].y != 0u) {
|
||||
let rot = data[at + 4u];
|
||||
let s = sin(-rot);
|
||||
let c = cos(-rot);
|
||||
let rx = dx * c - dy * s;
|
||||
let ry = dx * s + dy * c;
|
||||
d = sample_bilinear_clamped(aux, P.f[0].y + rx, P.f[0].z + ry).x - radius;
|
||||
} else if (n > 0u) {
|
||||
let off = u32(data[at + 11u]);
|
||||
let ud = fg2_outline_dist(off, n, dx, dy);
|
||||
d = select(ud, -ud, fg2_point_in_poly(off, n, dx, dy));
|
||||
} else {
|
||||
d = sqrt(dx * dx + dy * dy) - radius;
|
||||
}
|
||||
let ad = abs(d);
|
||||
let cov = clamp((hw - ad) / aa + 0.5, 0.0, 1.0);
|
||||
if (cov <= 0.0) {
|
||||
continue;
|
||||
}
|
||||
let u = clamp(d / hw, -1.0, 1.0);
|
||||
let col = vec4<f32>(data[at + 6u], data[at + 7u], data[at + 8u], data[at + 9u]);
|
||||
let a = cov * fg2_rw_profile(P.u[0].z, u) * data[at + 5u] * data[at + 10u] * col.w;
|
||||
o = fg2_over(o, vec4<f32>(col.xyz * a, a));
|
||||
}
|
||||
textureStore(out, p, o);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Beam (generate2::beam)
|
||||
// u[0] = (3D perspective, composite, length > 0); f[0] = (sx, sy, dx, dy); f[1] = (t0, t1,
|
||||
// start thickness, end thickness); f[2] = (softness, |d|², ...); f[3] = inside, f[4] = outside.
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fg2_beam(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fg2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let px = textureLoad(src, p, 0);
|
||||
let s = P.f[0].xy;
|
||||
let d = P.f[0].zw;
|
||||
let t0 = P.f[1].x;
|
||||
let t1 = P.f[1].y;
|
||||
let soft = P.f[2].x;
|
||||
let vx = f32(p.x) + 0.5 - s.x;
|
||||
let vy = f32(p.y) + 0.5 - s.y;
|
||||
let tc = clamp((vx * d.x + vy * d.y) / P.f[2].y, t0, t1);
|
||||
let ex = vx - d.x * tc;
|
||||
let ey = vy - d.y * tc;
|
||||
let dist = sqrt(ex * ex + ey * ey);
|
||||
var tk = tc;
|
||||
if (!(P.u[0].x != 0u || t1 - t0 < 1e-9)) {
|
||||
tk = (tc - t0) / (t1 - t0);
|
||||
}
|
||||
let hw = max((P.f[1].z + (P.f[1].w - P.f[1].z) * tk) * 0.5, 0.25);
|
||||
let tt = dist / hw;
|
||||
var a = 0.0;
|
||||
if (P.u[0].z == 0u) {
|
||||
a = 0.0;
|
||||
} else if (soft > 1e-3) {
|
||||
a = 1.0 - fg2_smoothstep(1.0 - soft, 1.0, tt);
|
||||
} else {
|
||||
a = clamp(hw - dist + 0.5, 0.0, 1.0);
|
||||
}
|
||||
let cc = P.f[3].xyz + (P.f[4].xyz - P.f[3].xyz) * fg2_smoothstep(0.0, 1.0, tt);
|
||||
let g = vec4<f32>(cc * a, a);
|
||||
if (P.u[0].y != 0u) {
|
||||
textureStore(out, p, fg2_over(px, g));
|
||||
} else {
|
||||
textureStore(out, p, g);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Lens Flare (generate2::lens_flare)
|
||||
// data: ghosts (t, r, colour rgb, k, ring); u[0] = ghosts; f[0] = (fx, fy, cx, cy);
|
||||
// f[1] = (diagonal, core k, rays, halo r); f[2] = (brightness, blend).
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fg2_flare(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fg2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let px = textureLoad(src, p, 0);
|
||||
let f = P.f[0].xy;
|
||||
let c = P.f[0].zw;
|
||||
let diag = P.f[1].x;
|
||||
let dx = f32(p.x) + 0.5 - f.x;
|
||||
let dy = f32(p.y) + 0.5 - f.y;
|
||||
let r = sqrt(dx * dx + dy * dy) / diag;
|
||||
let th = atan2(dy, dx);
|
||||
let ck = r / P.f[1].y;
|
||||
let core = 1.5 * exp(-(ck * ck)) + 0.25 * exp(-r / 0.12);
|
||||
let streak = powz(abs(cos(P.f[1].z * 0.5 * th)), 40.0) * exp(-r / 0.3) * 0.35;
|
||||
let hk = (r - P.f[1].w) / 0.012;
|
||||
let halo = exp(-(hk * hk)) * 0.12;
|
||||
let base = core + streak;
|
||||
var l = vec3<f32>(base, base * 0.95, base * 0.85);
|
||||
l += vec3<f32>(halo * 0.6, halo * 0.8, halo);
|
||||
for (var i = 0u; i < P.u[0].x; i++) {
|
||||
let at = i * 7u;
|
||||
let gt = data[at];
|
||||
let gr = data[at + 1u];
|
||||
let gx = f.x + (c.x - f.x) * gt;
|
||||
let gy = f.y + (c.y - f.y) * gt;
|
||||
let ex = f32(p.x) + 0.5 - gx;
|
||||
let ey = f32(p.y) + 0.5 - gy;
|
||||
let d = sqrt(ex * ex + ey * ey) / diag;
|
||||
var v: f32;
|
||||
if (data[at + 6u] != 0.0) {
|
||||
let q = (d - gr) / (gr * 0.12);
|
||||
v = exp(-(q * q));
|
||||
} else {
|
||||
v = fg2_smoothstep(gr, gr * 0.8, d);
|
||||
}
|
||||
l += vec3<f32>(data[at + 2u], data[at + 3u], data[at + 4u]) * data[at + 5u] * v;
|
||||
}
|
||||
l = l * P.f[2].x;
|
||||
let la = min(max(max(l.x, l.y), l.z), 1.0);
|
||||
let k = clamp(la, 0.0, 1.0);
|
||||
let o = vec4<f32>(px.xyz + l, clamp(px.w + k * (1.0 - px.w), 0.0, 1.0));
|
||||
textureStore(out, p, o + (px - o) * P.f[2].y);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Glue Gun (generate3::glue_gun)
|
||||
// data: beads (x, y, radius); u[0] = (beads, glue only); f[0] = (light x, y, z, strength);
|
||||
// f[1].x = strength weight (clamp(strength, 0, 1) max 0.3).
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fg2_glue(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fg2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let px = textureLoad(src, p, 0);
|
||||
let glue_only = P.u[0].y != 0u;
|
||||
let fx = f32(p.x) + 0.5;
|
||||
let fy = f32(p.y) + 0.5;
|
||||
var best_h = 0.0;
|
||||
var best_d = vec2<f32>(0.0);
|
||||
var best_r = 1.0;
|
||||
for (var i = 0u; i < P.u[0].x; i++) {
|
||||
let r = data[i * 3u + 2u];
|
||||
let dx = (fx - data[i * 3u]) / r;
|
||||
let dy = (fy - data[i * 3u + 1u]) / r;
|
||||
let d2 = dx * dx + dy * dy;
|
||||
if (d2 < 1.0) {
|
||||
let hgt = sqrt(1.0 - d2);
|
||||
if (hgt > best_h) {
|
||||
best_h = hgt;
|
||||
best_d = vec2<f32>(dx, dy);
|
||||
best_r = r;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (best_h <= 0.0) {
|
||||
textureStore(out, p, select(px, vec4<f32>(0.0), glue_only));
|
||||
return;
|
||||
}
|
||||
let nrm = vec3<f32>(best_d, best_h);
|
||||
let ndl = max(dot(nrm, P.f[0].xyz), 0.0);
|
||||
let spec = powz(ndl, 24.0);
|
||||
let strength = P.f[0].w;
|
||||
let s = sample_bilinear_clamped(src, fx - nrm.x * best_r * 0.5 * strength, fy - nrm.y * best_r * 0.5 * strength);
|
||||
let edge = fg2_smoothstep(0.0, 0.15, best_h);
|
||||
let shade = 0.65 + 0.35 * ndl;
|
||||
let g = vec4<f32>(s.xyz * shade + vec3<f32>(spec), max(s.w, 0.6));
|
||||
let gp = vec4<f32>(min(g.xyz, vec3<f32>(g.w + spec)), min(g.w, 1.0));
|
||||
var base = px;
|
||||
if (glue_only) {
|
||||
base = vec4<f32>(0.0);
|
||||
}
|
||||
var o = base + (gp - base) * (edge * P.f[1].x);
|
||||
o.w = clamp(o.w, 0.0, 1.0);
|
||||
textureStore(out, p, o);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Threads (generate3::threads)
|
||||
// u[0] = (overlaps); f[0] = (cx, cy, sin, cos); f[1] = (thread width, height, coverage,
|
||||
// shadowing); f[2].x = texture.
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fg2_threads(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fg2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let c = P.f[0].xy;
|
||||
let sn = P.f[0].z;
|
||||
let cs = P.f[0].w;
|
||||
let tw = P.f[1].x;
|
||||
let th = P.f[1].y;
|
||||
let coverage = P.f[1].z;
|
||||
let shadow = P.f[1].w;
|
||||
let tex_amt = P.f[2].x;
|
||||
let overlaps = i32(P.u[0].x);
|
||||
let dx = f32(p.x) + 0.5 - c.x;
|
||||
let dy = f32(p.y) + 0.5 - c.y;
|
||||
let u = dx * cs + dy * sn;
|
||||
let v = -dx * sn + dy * cs;
|
||||
let ci = i32(floor(u / tw));
|
||||
let cj = i32(floor(v / th));
|
||||
let fu = u / tw - f32(ci);
|
||||
let fv = v / th - f32(cj);
|
||||
let in_h = abs(fv - 0.5) < coverage * 0.5;
|
||||
let in_v = abs(fu - 0.5) < coverage * 0.5;
|
||||
let h_top = ((ci + cj) % (2 * overlaps) + 2 * overlaps) % (2 * overlaps) < overlaps;
|
||||
var horizontal: bool;
|
||||
if (in_h && in_v) {
|
||||
horizontal = h_top;
|
||||
} else if (in_h) {
|
||||
horizontal = true;
|
||||
} else if (in_v) {
|
||||
horizontal = false;
|
||||
} else {
|
||||
textureStore(out, p, vec4<f32>(0.0));
|
||||
return;
|
||||
}
|
||||
var su = u;
|
||||
var sv = (f32(cj) + 0.5) * th;
|
||||
if (!horizontal) {
|
||||
su = (f32(ci) + 0.5) * tw;
|
||||
sv = v;
|
||||
}
|
||||
let s = sample_bilinear_clamped(src, c.x + su * cs - sv * sn, c.y + su * sn + sv * cs);
|
||||
let half_cov = max(coverage * 0.5, 1e-6);
|
||||
var across = (fu - 0.5) / half_cov;
|
||||
var along = fv;
|
||||
if (horizontal) {
|
||||
across = (fv - 0.5) / half_cov;
|
||||
along = fu;
|
||||
}
|
||||
let profile = sqrt(max(1.0 - across * across, 0.0));
|
||||
let dive = 1.0 - abs(along - 0.5) * 2.0;
|
||||
let under = select(1.0, 0.0, horizontal == h_top);
|
||||
let shade = 1.0 - shadow * (1.0 - profile) * 0.8 - shadow * 0.25 * (1.0 - dive) * under;
|
||||
var fib = 1.0;
|
||||
if (tex_amt > 0.0) {
|
||||
let seed = bitcast<u32>(ci) ^ (bitcast<u32>(cj) << 8u);
|
||||
fib = 1.0 - tex_amt * 0.3 * hash_noise(u32(max(along * 64.0, 0.0)), u32(max(across * 8.0 + 8.0, 0.0)), seed);
|
||||
}
|
||||
let k = max(shade * fib, 0.0);
|
||||
textureStore(out, p, vec4<f32>(s.xyz * k, s.w));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Paint Bucket / Eyedropper Fill (generate3)
|
||||
|
||||
// Paint Bucket's fill: aux = coverage (x); u[0] = (mode); f[0] = colour; f[1].x = opacity.
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fg2_bucket(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fg2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let px = textureLoad(src, p, 0);
|
||||
let c = textureLoad(aux, p, 0).x;
|
||||
let col = P.f[0];
|
||||
let mode = P.u[0].x;
|
||||
let k = clamp(c * P.f[1].x * col.w, 0.0, 1.0);
|
||||
if (k <= 0.0) {
|
||||
textureStore(out, p, select(px, vec4<f32>(0.0), mode == 9u));
|
||||
return;
|
||||
}
|
||||
let dc = fg2_unpremul(px);
|
||||
let da = select(max(px.w, 0.0), px.w, px.w > 1e-6);
|
||||
let fc = col.xyz;
|
||||
var mixed = fc;
|
||||
switch mode {
|
||||
case 1u: {
|
||||
mixed = dc + fc;
|
||||
}
|
||||
case 2u: {
|
||||
mixed = dc * fc;
|
||||
}
|
||||
case 3u: {
|
||||
mixed = vec3<f32>(1.0) - (vec3<f32>(1.0) - dc) * (vec3<f32>(1.0) - fc);
|
||||
}
|
||||
case 4u, 5u, 6u, 7u: {
|
||||
let d = rgb_to_hsl(dc);
|
||||
let f = rgb_to_hsl(fc);
|
||||
switch mode {
|
||||
case 4u: {
|
||||
mixed = hsl_to_rgb(f.x, d.y, d.z);
|
||||
}
|
||||
case 5u: {
|
||||
mixed = hsl_to_rgb(d.x, f.y, d.z);
|
||||
}
|
||||
case 6u: {
|
||||
mixed = hsl_to_rgb(f.x, f.y, d.z);
|
||||
}
|
||||
default: {
|
||||
mixed = hsl_to_rgb(d.x, d.y, f.z);
|
||||
}
|
||||
}
|
||||
}
|
||||
default: {}
|
||||
}
|
||||
var o: vec4<f32>;
|
||||
switch mode {
|
||||
case 8u: {
|
||||
o = fg2_over(vec4<f32>(fc * k, k), px);
|
||||
}
|
||||
case 9u: {
|
||||
o = vec4<f32>(fc * k, k);
|
||||
}
|
||||
case 0u: {
|
||||
o = fg2_over(px, vec4<f32>(fc * k, k));
|
||||
}
|
||||
default: {
|
||||
if (da <= 1e-6) {
|
||||
o = px;
|
||||
} else {
|
||||
let c3 = dc + (mixed - dc) * k;
|
||||
o = vec4<f32>(c3 * da, da);
|
||||
}
|
||||
}
|
||||
}
|
||||
textureStore(out, p, o);
|
||||
}
|
||||
|
||||
// Paint Bucket's coverage from the region (x): u[0].x = 0 antialias (aux = the blurred region),
|
||||
// 1 = view threshold, 2 = stroke ring (aux = the eroded region, src = the dilated one).
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fg2_bucket_cov(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fg2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let a = textureLoad(src, p, 0).x;
|
||||
let b = textureLoad(aux, p, 0).x;
|
||||
switch P.u[0].x {
|
||||
case 0u: {
|
||||
// src = the region, aux = its blur.
|
||||
textureStore(out, p, vec4<f32>(select(b, max(b, 0.5), a > 0.0), 0.0, 0.0, 0.0));
|
||||
}
|
||||
case 1u: {
|
||||
textureStore(out, p, vec4<f32>(a, a, a, 1.0));
|
||||
}
|
||||
default: {
|
||||
textureStore(out, p, vec4<f32>(max(a - b, 0.0), 0.0, 0.0, 0.0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Eyedropper Fill: u[0].x = keep alpha; f[0] = fill (straight, alpha); f[1].x = blend.
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fg2_eyedropper(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fg2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let px = textureLoad(src, p, 0);
|
||||
let a = select(P.f[0].w, px.w, P.u[0].x != 0u);
|
||||
let f = vec4<f32>(P.f[0].xyz * a, a);
|
||||
textureStore(out, p, f + (px - f) * P.f[1].x);
|
||||
}
|
||||
|
|
@ -0,0 +1,947 @@
|
|||
// GPU effects, pixel ports (part C): see src/fx_pixel2.rs. Every entry point is prefixed
|
||||
// `fp2_` and mirrors the CPU effect operation for operation; positions are pixel centres
|
||||
// relative to the effect's centre or anchor so f32 keeps its precision.
|
||||
|
||||
fn fp2_inside(p: vec2<i32>) -> bool {
|
||||
let d = out_dims();
|
||||
return p.x < d.x && p.y < d.y;
|
||||
}
|
||||
|
||||
// util::unpremul (colour 0 below alpha 1e-6).
|
||||
fn fp2_unpremul(p: vec4<f32>) -> vec3<f32> {
|
||||
if (p.w > 1e-6) {
|
||||
return p.xyz / p.w;
|
||||
}
|
||||
return vec3<f32>(0.0);
|
||||
}
|
||||
|
||||
// util::smoothstep.
|
||||
fn fp2_smoothstep(e0: f32, e1: f32, x: f32) -> f32 {
|
||||
if (abs(e1 - e0) < 1e-9) {
|
||||
return select(1.0, 0.0, x < e0);
|
||||
}
|
||||
let t = clamp((x - e0) / (e1 - e0), 0.0, 1.0);
|
||||
return t * t * (3.0 - 2.0 * t);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Cross Blur (blur2::cross_blur)
|
||||
// src = horizontal blur, aux = vertical blur; u[0].x = transfer mode.
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fp2_cross(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fp2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let a = textureLoad(src, p, 0);
|
||||
let v = textureLoad(aux, p, 0);
|
||||
var o: vec4<f32>;
|
||||
switch P.u[0].x {
|
||||
case 1u: {
|
||||
o = a + v;
|
||||
}
|
||||
case 2u: {
|
||||
o = a + v - a * v;
|
||||
}
|
||||
case 3u: {
|
||||
o = max(a, v);
|
||||
}
|
||||
default: {
|
||||
o = (a + v) * 0.5;
|
||||
}
|
||||
}
|
||||
o.w = clamp(o.w, 0.0, 1.0);
|
||||
textureStore(out, p, o);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Radial Blur (blur3::cc_radial_blur)
|
||||
// u[0].x = type; f[0] = (cx, cy, zoom, arc); f[1].x = quality.
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fp2_radial(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fp2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let kind = P.u[0].x;
|
||||
let c = P.f[0].xy;
|
||||
let zoom = P.f[0].z;
|
||||
let arc = P.f[0].w;
|
||||
let dx = f32(p.x) + 0.5 - c.x;
|
||||
let dy = f32(p.y) + 0.5 - c.y;
|
||||
let r = sqrt(dx * dx + dy * dy);
|
||||
var span = r * abs(zoom);
|
||||
if (kind >= 3u) {
|
||||
span = r * abs(arc);
|
||||
}
|
||||
let n = u32(clamp(ceil(span * P.f[1].x / 50.0), 1.0, 256.0));
|
||||
if (n <= 1u) {
|
||||
textureStore(out, p, textureLoad(src, p, 0));
|
||||
return;
|
||||
}
|
||||
var acc = vec4<f32>(0.0);
|
||||
var wsum = 0.0;
|
||||
for (var i = 0u; i < n; i++) {
|
||||
let t = f32(i) / f32(n - 1u);
|
||||
var s = vec2<f32>(0.0);
|
||||
var w = 1.0;
|
||||
switch kind {
|
||||
case 0u: {
|
||||
let k = 1.0 - zoom * t;
|
||||
s = vec2<f32>(c.x + dx * k, c.y + dy * k);
|
||||
}
|
||||
case 1u: {
|
||||
let k = 1.0 - zoom * t;
|
||||
s = vec2<f32>(c.x + dx * k, c.y + dy * k);
|
||||
w = (1.0 - t) + 0.05;
|
||||
}
|
||||
case 2u: {
|
||||
let k = 1.0 + zoom * (t - 0.5);
|
||||
s = vec2<f32>(c.x + dx * k, c.y + dy * k);
|
||||
}
|
||||
default: {
|
||||
let a = arc * (t - 0.5);
|
||||
let sn = sin(a);
|
||||
let co = cos(a);
|
||||
if (kind == 4u) {
|
||||
w = (1.0 - abs(2.0 * t - 1.0)) + 0.05;
|
||||
}
|
||||
s = vec2<f32>(c.x + dx * co - dy * sn, c.y + dx * sn + dy * co);
|
||||
}
|
||||
}
|
||||
acc += sample_bilinear(src, s.x, s.y) * w;
|
||||
wsum += w;
|
||||
}
|
||||
textureStore(out, p, acc / wsum);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- util::gauss_plane
|
||||
// One box pass (edges repeated) summing each window directly: the CPU's planes accumulate
|
||||
// in f64, so a running f32 sum would lose the small values (normalised blurs divide them).
|
||||
// u[0] = (radius, vertical).
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fp2_box(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fp2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let r = i32(P.u[0].x);
|
||||
var acc = vec4<f32>(0.0);
|
||||
for (var i = -r; i <= r; i++) {
|
||||
if (P.u[0].y != 0u) {
|
||||
acc += tex_get_clamped(src, p.x, p.y + i);
|
||||
} else {
|
||||
acc += tex_get_clamped(src, p.x + i, p.y);
|
||||
}
|
||||
}
|
||||
textureStore(out, p, acc * (1.0 / f32(2 * r + 1)));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- planes
|
||||
// One value per pixel into x. u[0] = (kind, property): kind 0 = luminance of the premultiplied
|
||||
// pixel (blur2::plum); 1 = CC Glass / Plastic height (stylize3::height_field: the property of
|
||||
// the straight colour times alpha, alpha itself); 2 = CC Mr. Smoothie's flow property (straight);
|
||||
// 3 = x clamped to [f[0].x, f[0].y] (CC Plastic's cut).
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fp2_plane(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fp2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let px = textureLoad(src, p, 0);
|
||||
let prop = P.u[0].y;
|
||||
var v = 0.0;
|
||||
if (P.u[0].x == 0u) {
|
||||
v = luminance(px.xyz);
|
||||
} else if (P.u[0].x == 3u) {
|
||||
v = clamp(px.x, P.f[0].x, P.f[0].y);
|
||||
} else {
|
||||
let c = fp2_unpremul(px);
|
||||
let a = px.w;
|
||||
switch prop {
|
||||
case 0u: {
|
||||
v = c.x;
|
||||
}
|
||||
case 1u: {
|
||||
v = c.y;
|
||||
}
|
||||
case 2u: {
|
||||
v = c.z;
|
||||
}
|
||||
case 3u: {
|
||||
v = a;
|
||||
}
|
||||
case 4u: {
|
||||
v = luminance(c);
|
||||
}
|
||||
case 5u: {
|
||||
v = rgb_to_hsl(c).z;
|
||||
}
|
||||
case 6u: {
|
||||
v = rgb_to_hsl(c).x;
|
||||
}
|
||||
default: {
|
||||
v = rgb_to_hsl(c).y;
|
||||
}
|
||||
}
|
||||
if (P.u[0].x == 1u && prop != 3u) {
|
||||
v = v * a;
|
||||
}
|
||||
}
|
||||
textureStore(out, p, vec4<f32>(v, 0.0, 0.0, 0.0));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Vector Blur (blur2::vector_blur)
|
||||
// aux = softened luminance map (x). u[0].x = type; f[0] = (amount, angle offset).
|
||||
|
||||
fn fp2_map(x: i32, y: i32) -> f32 {
|
||||
return tex_get_clamped(aux, x, y).x;
|
||||
}
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fp2_vector(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fp2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let d = vec2<i32>(textureDimensions(aux));
|
||||
let x = p.x;
|
||||
let y = p.y;
|
||||
let gx = (fp2_map(min(x + 1, d.x - 1), y) - fp2_map(max(x - 1, 0), y)) * 0.5;
|
||||
let gy = (fp2_map(x, min(y + 1, d.y - 1)) - fp2_map(x, max(y - 1, 0))) * 0.5;
|
||||
let mag = sqrt(gx * gx + gy * gy);
|
||||
let m = fp2_map(x, y);
|
||||
let kind = P.u[0].x;
|
||||
let amount = P.f[0].x;
|
||||
let off = P.f[0].y;
|
||||
var ang = 0.0;
|
||||
var len = amount;
|
||||
if (kind <= 2u) {
|
||||
if (mag < 1e-6) {
|
||||
textureStore(out, p, textureLoad(src, p, 0));
|
||||
return;
|
||||
}
|
||||
let g = atan2(gy, gx);
|
||||
var base = g + 1.5707963267948966;
|
||||
if (kind == 2u) {
|
||||
base = g;
|
||||
}
|
||||
if (kind != 1u) {
|
||||
len = amount * min(mag * 10.0, 1.0);
|
||||
}
|
||||
ang = base + off;
|
||||
} else {
|
||||
ang = m * 6.283185307179586 + off;
|
||||
if (kind != 3u) {
|
||||
len = amount * m;
|
||||
}
|
||||
}
|
||||
if (len < 0.5) {
|
||||
textureStore(out, p, textureLoad(src, p, 0));
|
||||
return;
|
||||
}
|
||||
let dx = cos(ang) * len;
|
||||
let dy = sin(ang) * len;
|
||||
let n = u32(clamp(ceil(len * 2.0), 2.0, 48.0));
|
||||
var acc = vec4<f32>(0.0);
|
||||
for (var i = 0u; i < n; i++) {
|
||||
let t = f32(i) / f32(n - 1u) - 0.5;
|
||||
acc += sample_bilinear_clamped(src, f32(x) + 0.5 + dx * t, f32(y) + 0.5 + dy * t) / f32(n);
|
||||
}
|
||||
textureStore(out, p, acc);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- per-pixel colour effects
|
||||
// u[0].x = effect: 0 Cineon Converter, 1 HDR Compander, 2 HDR Highlight Compression,
|
||||
// 3 CC Overbrights, 4 Color Link (alpha), 5 Color Link (blend), 6 CC Composite.
|
||||
|
||||
// utility::Cineon::log_to_lin; c = (offset, knee, white, gamma), r = (internal black, white).
|
||||
fn fp2_log_to_lin(v: f32, c: vec4<f32>, r: vec2<f32>) -> f32 {
|
||||
let code = v * 1023.0;
|
||||
var lin = (pow(10.0, (code - c.z) * 0.002 / 0.6) - c.x) / max(1.0 - c.x, 1e-6);
|
||||
lin = powz(max(lin, 0.0), 1.7 / max(c.w, 0.01));
|
||||
let k = c.y;
|
||||
if (k < 1.0 && lin > k) {
|
||||
let w = 1.0 - k;
|
||||
lin = k + w * (1.0 - exp(-(lin - k) / w));
|
||||
}
|
||||
return r.x + (r.y - r.x) * lin;
|
||||
}
|
||||
|
||||
// utility::Cineon::lin_to_log.
|
||||
fn fp2_lin_to_log(v: f32, c: vec4<f32>, r: vec2<f32>) -> f32 {
|
||||
let d = r.y - r.x;
|
||||
var lin = (v - r.x) / select(d, 1e-6, abs(d) < 1e-6);
|
||||
let k = c.y;
|
||||
if (k < 1.0 && lin > k) {
|
||||
let w = 1.0 - k;
|
||||
lin = k - w * log(max(1.0 - (lin - k) / w, 1e-6));
|
||||
}
|
||||
lin = powz(max(lin, 0.0), max(c.w, 0.01) / 1.7);
|
||||
let code = c.z + log(max(lin * (1.0 - c.x) + c.x, 1e-9)) * 0.4342944819032518 * 0.6 / 0.002;
|
||||
return code / 1023.0;
|
||||
}
|
||||
|
||||
// utility2::spow.
|
||||
fn fp2_spow(v: f32, e: f32) -> f32 {
|
||||
return sign(v) * powz(abs(v), e);
|
||||
}
|
||||
|
||||
fn fp2_cineon(v: f32) -> f32 {
|
||||
let c = P.f[0];
|
||||
let r = P.f[1].xy;
|
||||
switch P.u[0].y {
|
||||
case 0u: {
|
||||
return fp2_lin_to_log(v, c, r);
|
||||
}
|
||||
case 1u: {
|
||||
return fp2_log_to_lin(v, c, r);
|
||||
}
|
||||
default: {
|
||||
return fp2_lin_to_log(fp2_log_to_lin(v, c, r), P.f[2], vec2<f32>(0.0, 1.0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fp2_point(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fp2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let px = textureLoad(src, p, 0);
|
||||
var o = px;
|
||||
switch P.u[0].x {
|
||||
// f[0], f[1], f[2]: see fp2_cineon; u[0].y = conversion type.
|
||||
case 0u: {
|
||||
if (px.w > 0.0) {
|
||||
let c = px.xyz / px.w;
|
||||
o = vec4<f32>(vec3<f32>(fp2_cineon(c.x), fp2_cineon(c.y), fp2_cineon(c.z)) * px.w, px.w);
|
||||
}
|
||||
}
|
||||
// u[0].y = expand; f[0] = (gain, gamma).
|
||||
case 1u: {
|
||||
if (px.w > 0.0) {
|
||||
let c = fp2_unpremul(px);
|
||||
let g = P.f[0].x;
|
||||
let e = P.f[0].y;
|
||||
var r = vec3<f32>(0.0);
|
||||
if (P.u[0].y != 0u) {
|
||||
r = vec3<f32>(fp2_spow(c.x, e), fp2_spow(c.y, e), fp2_spow(c.z, e)) * g;
|
||||
} else {
|
||||
r = vec3<f32>(fp2_spow(c.x / g, 1.0 / e), fp2_spow(c.y / g, 1.0 / e), fp2_spow(c.z / g, 1.0 / e));
|
||||
}
|
||||
o = vec4<f32>(r * px.w, px.w);
|
||||
}
|
||||
}
|
||||
// f[0].x = knee.
|
||||
case 2u: {
|
||||
if (px.w > 0.0) {
|
||||
let c = px.xyz / px.w;
|
||||
let m = max(max(c.x, c.y), c.z);
|
||||
let knee = P.f[0].x;
|
||||
if (m > knee) {
|
||||
let w = 1.0 - knee;
|
||||
let k = (knee + w * (m - knee) / ((m - knee) + w)) / m;
|
||||
o = vec4<f32>(c * k * px.w, px.w);
|
||||
}
|
||||
}
|
||||
}
|
||||
// u[0].y = channel; f[0] = highlight colour.
|
||||
case 3u: {
|
||||
if (px.w > 0.0) {
|
||||
let c = fp2_unpremul(px);
|
||||
var v = max(max(c.x, c.y), c.z);
|
||||
switch P.u[0].y {
|
||||
case 1u: {
|
||||
v = c.x;
|
||||
}
|
||||
case 2u: {
|
||||
v = c.y;
|
||||
}
|
||||
case 3u: {
|
||||
v = c.z;
|
||||
}
|
||||
case 4u: {
|
||||
v = px.w;
|
||||
}
|
||||
case 5u: {
|
||||
v = luminance(c);
|
||||
}
|
||||
default: {}
|
||||
}
|
||||
var r = vec3<f32>(v * 0.35);
|
||||
if (v > 1.0) {
|
||||
r = P.f[0].xyz;
|
||||
} else if (v < 0.0) {
|
||||
r = vec3<f32>(1.0) - P.f[0].xyz;
|
||||
}
|
||||
o = vec4<f32>(r * px.w, px.w);
|
||||
}
|
||||
}
|
||||
// f[0] = (sampled alpha, opacity); u[0].y = stencil.
|
||||
case 4u: {
|
||||
let c = fp2_unpremul(px);
|
||||
let a0 = select(max(px.w, 0.0), px.w, px.w > 1e-6);
|
||||
var na = a0 + (P.f[0].x - a0) * P.f[0].y;
|
||||
if (P.u[0].y != 0u) {
|
||||
na = min(na, a0);
|
||||
}
|
||||
let a = clamp(na, 0.0, 1.0);
|
||||
o = vec4<f32>(c * a, a);
|
||||
}
|
||||
// f[0] = premultiplied sampled colour; u[0] = (stencil, mode).
|
||||
case 5u: {
|
||||
o = blend_pixel(P.u[0].z, px, P.f[0], 0.5);
|
||||
o.w = clamp(o.w, 0.0, 1.0);
|
||||
if (P.u[0].y != 0u) {
|
||||
o = vec4<f32>(fp2_unpremul(o) * px.w, px.w);
|
||||
}
|
||||
}
|
||||
// u[0] = (_, composite original, mode, rgb only); f[0].x = opacity.
|
||||
default: {
|
||||
let orig = px * P.f[0].x;
|
||||
if (P.u[0].y == 1u) {
|
||||
o = blend_pixel(P.u[0].z, orig, px, 0.5);
|
||||
} else {
|
||||
o = blend_pixel(P.u[0].z, px, orig, 0.5);
|
||||
}
|
||||
o.w = clamp(o.w, 0.0, 1.0);
|
||||
if (P.u[0].w != 0u) {
|
||||
o = vec4<f32>(fp2_unpremul(o) * px.w, px.w);
|
||||
}
|
||||
}
|
||||
}
|
||||
textureStore(out, p, o);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Block Load (stylize3::block_load)
|
||||
|
||||
// Mean colour of every s×s block: one invocation per block (the CPU's summation order).
|
||||
// u[0].x = s.
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fp2_block_means(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fp2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let s = i32(P.u[0].x);
|
||||
let d = vec2<i32>(textureDimensions(src));
|
||||
var acc = vec4<f32>(0.0);
|
||||
var n = 0.0;
|
||||
for (var y = p.y * s; y < min((p.y + 1) * s, d.y); y++) {
|
||||
for (var x = p.x * s; x < min((p.x + 1) * s, d.x); x++) {
|
||||
acc += textureLoad(src, vec2<i32>(x, y), 0);
|
||||
n += 1.0;
|
||||
}
|
||||
}
|
||||
if (n > 0.0) {
|
||||
acc = acc / n;
|
||||
}
|
||||
textureStore(out, p, acc);
|
||||
}
|
||||
|
||||
// stylize3::block_at on grid `t` (block size `s`).
|
||||
fn fp2_block_at(t: texture_2d<f32>, s: u32, x: i32, y: i32, bilinear: bool) -> vec4<f32> {
|
||||
let g = vec2<i32>(textureDimensions(t));
|
||||
if (!bilinear || s == 1u) {
|
||||
return textureLoad(t, vec2<i32>(min(x / i32(s), g.x - 1), min(y / i32(s), g.y - 1)), 0);
|
||||
}
|
||||
let fx = (f32(x) + 0.5) / f32(s) - 0.5;
|
||||
let fy = (f32(y) + 0.5) / f32(s) - 0.5;
|
||||
let x0 = floor(fx);
|
||||
let y0 = floor(fy);
|
||||
let tx = fx - x0;
|
||||
let ty = fy - y0;
|
||||
let xa = clamp(i32(x0), 0, g.x - 1);
|
||||
let xb = clamp(i32(x0 + 1.0), 0, g.x - 1);
|
||||
let ya = clamp(i32(y0), 0, g.y - 1);
|
||||
let yb = clamp(i32(y0 + 1.0), 0, g.y - 1);
|
||||
let a = textureLoad(t, vec2<i32>(xa, ya), 0);
|
||||
let b = textureLoad(t, vec2<i32>(xb, ya), 0);
|
||||
let c = textureLoad(t, vec2<i32>(xa, yb), 0);
|
||||
let e = textureLoad(t, vec2<i32>(xb, yb), 0);
|
||||
let top = a + (b - a) * tx;
|
||||
let bot = c + (e - c) * tx;
|
||||
return top + (bot - top) * ty;
|
||||
}
|
||||
|
||||
// src = the new level's grid, aux = the previous level's grid (or the new one).
|
||||
// u[0] = (new block size, old block size, flags: 1 scanlines, 2 smoothing, 4 bilinear,
|
||||
// 8 previous level exists, 16 start cleared); f[0] = (frac, rows of the new level).
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fp2_block_load(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fp2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let s_new = P.u[0].x;
|
||||
let flags = P.u[0].z;
|
||||
let scan = (flags & 1u) != 0u;
|
||||
let smoothing = (flags & 2u) != 0u;
|
||||
let bilinear = (flags & 4u) != 0u;
|
||||
let frac = P.f[0].x;
|
||||
let soft = select(0.0, 1.5, smoothing);
|
||||
var t = 0.0;
|
||||
if (scan) {
|
||||
let front = frac * (P.f[0].y + soft) - soft * 0.5;
|
||||
let r = f32(p.y / i32(s_new)) + 0.5;
|
||||
if (soft > 0.0) {
|
||||
t = fp2_smoothstep(r - soft * 0.5, r + soft * 0.5, front);
|
||||
} else if (r < front) {
|
||||
t = 1.0;
|
||||
}
|
||||
} else if (smoothing) {
|
||||
t = frac;
|
||||
}
|
||||
var n = vec4<f32>(0.0);
|
||||
if (t > 0.0) {
|
||||
n = fp2_block_at(src, s_new, p.x, p.y, bilinear);
|
||||
}
|
||||
var prev = vec4<f32>(0.0);
|
||||
if ((flags & 8u) != 0u) {
|
||||
prev = fp2_block_at(aux, P.u[0].y, p.x, p.y, bilinear);
|
||||
} else if ((flags & 16u) == 0u) {
|
||||
prev = fp2_block_at(src, s_new, p.x, p.y, bilinear);
|
||||
}
|
||||
var o = prev;
|
||||
if (t >= 1.0) {
|
||||
o = n;
|
||||
} else if (t > 0.0) {
|
||||
o = prev + (n - prev) * t;
|
||||
}
|
||||
textureStore(out, p, o);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Burn Film (stylize3::burn_film)
|
||||
// u[0].x = seed + 17; f[0] = (cx, cy, diagonal, feature size); f[1] = (threshold, band).
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fp2_burn(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fp2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let px = textureLoad(src, p, 0);
|
||||
let fx = f32(p.x) + 0.5;
|
||||
let fy = f32(p.y) + 0.5;
|
||||
let dx = fx - P.f[0].x;
|
||||
let dy = fy - P.f[0].y;
|
||||
let d = sqrt(dx * dx + dy * dy) / P.f[0].z;
|
||||
let n = fxs_fbm(fx / P.f[0].w, fy / P.f[0].w, 0.0, P.u[0].x, 4.0);
|
||||
let f = n * 0.65 + (1.0 - min(d, 1.0)) * 0.35;
|
||||
let th = P.f[1].x;
|
||||
let band = P.f[1].y;
|
||||
var o = px;
|
||||
if (f > th + band) {
|
||||
o = vec4<f32>(0.0);
|
||||
} else if (f > th - band * 2.0) {
|
||||
let c = fp2_unpremul(px);
|
||||
let k = fp2_smoothstep(th - band * 2.0, th, f);
|
||||
let hot = fp2_smoothstep(th, th + band, f);
|
||||
let chr = 1.0 - k * 0.85;
|
||||
var r = c * chr;
|
||||
r = vec3<f32>(r.x + (1.0 - r.x) * hot * 0.9, r.y + (0.45 - r.y) * hot * 0.9, r.z * (1.0 - hot));
|
||||
let na = px.w * (1.0 - hot * 0.6);
|
||||
o = vec4<f32>(r * na, na);
|
||||
}
|
||||
textureStore(out, p, o);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Glass / CC Plastic (stylize3)
|
||||
// aux = height field (x). u[0] = (glass, point light); f[0] = (height scale k, displacement,
|
||||
// height != 0, light height); f[1] = (intensity, ambient, diffuse, specular);
|
||||
// f[2] = (shininess, metal, light x, light y); f[3] = light colour; f[4] = distant direction.
|
||||
|
||||
fn fp2_h(x: i32, y: i32) -> f32 {
|
||||
return tex_get_clamped(aux, x, y).x;
|
||||
}
|
||||
|
||||
// stylize3::BumpLight::shade.
|
||||
fn fp2_shade(c: vec3<f32>, n: vec3<f32>, l: vec3<f32>) -> vec3<f32> {
|
||||
let intensity = P.f[1].x;
|
||||
let ambient = P.f[1].y;
|
||||
let diffuse = P.f[1].z;
|
||||
let specular = P.f[1].w;
|
||||
let ndl = max(dot(n, l), 0.0);
|
||||
let flat_z = max(l.z, 0.0);
|
||||
let base = ambient + diffuse * flat_z;
|
||||
var lit = 1.0;
|
||||
if (base > 1e-4) {
|
||||
lit = (ambient + diffuse * ndl) / base;
|
||||
}
|
||||
lit = 1.0 + (lit - 1.0) * intensity;
|
||||
let hv = vec3<f32>(l.x, l.y, l.z + 1.0);
|
||||
let hl = max(sqrt(hv.x * hv.x + hv.y * hv.y + hv.z * hv.z), 1e-6);
|
||||
let ndh = max((n.x * hv.x + n.y * hv.y + n.z * hv.z) / hl, 0.0);
|
||||
let spec = specular * intensity * powz(ndh, P.f[2].x);
|
||||
let lc = P.f[3].xyz;
|
||||
let metal = P.f[2].y;
|
||||
let hl_col = lc * (1.0 - metal) + c * lc * metal;
|
||||
return c * lit * (vec3<f32>(1.0) + (lc - vec3<f32>(1.0)) * min(intensity, 1.0)) + spec * hl_col;
|
||||
}
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fp2_bump(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fp2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let px = textureLoad(src, p, 0);
|
||||
let glass = P.u[0].x != 0u;
|
||||
if (px.w <= 0.0) {
|
||||
textureStore(out, p, select(px, vec4<f32>(0.0), glass));
|
||||
return;
|
||||
}
|
||||
let k = P.f[0].x;
|
||||
let gx = (fp2_h(p.x + 1, p.y) - fp2_h(p.x - 1, p.y)) * 0.5 * k;
|
||||
let gy = (fp2_h(p.x, p.y + 1) - fp2_h(p.x, p.y - 1)) * 0.5 * k;
|
||||
let ln = sqrt(gx * gx + gy * gy + 1.0);
|
||||
let n = vec3<f32>(-gx / ln, -gy / ln, 1.0 / ln);
|
||||
let fx = f32(p.x) + 0.5;
|
||||
let fy = f32(p.y) + 0.5;
|
||||
var l = P.f[4].xyz;
|
||||
if (P.u[0].y != 0u) {
|
||||
let v = vec3<f32>(P.f[2].z - fx, P.f[2].w - fy, max(P.f[0].w, 1.0));
|
||||
let vl = max(sqrt(v.x * v.x + v.y * v.y + v.z * v.z), 1e-6);
|
||||
l = v / vl;
|
||||
}
|
||||
var c = fp2_unpremul(px);
|
||||
if (glass) {
|
||||
let disp = P.f[0].y;
|
||||
if (disp != 0.0 && P.f[0].z != 0.0) {
|
||||
let s = sample_bilinear_clamped(src, fx + n.x * disp, fy + n.y * disp);
|
||||
if (s.w > 0.0) {
|
||||
c = fp2_unpremul(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
let o = fp2_shade(c, n, l);
|
||||
textureStore(out, p, vec4<f32>(o * px.w, px.w));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC HexTile (stylize3::hextile)
|
||||
// u[0].x = render mode; f[0] = (cx, cy, radius, smearing); f[1] = (sin, cos).
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fp2_hextile(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fp2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let cx = P.f[0].x;
|
||||
let cy = P.f[0].y;
|
||||
let r = P.f[0].z;
|
||||
let smear = P.f[0].w;
|
||||
let sn = P.f[1].x;
|
||||
let cs = P.f[1].y;
|
||||
let mode = P.u[0].x;
|
||||
let dx = f32(p.x) + 0.5 - cx;
|
||||
let dy = f32(p.y) + 0.5 - cy;
|
||||
let u = dx * cs + dy * sn;
|
||||
let v = -dx * sn + dy * cs;
|
||||
// stylize3::hex_cell (cube rounding).
|
||||
let q = (0.5773502691896257 * u - v / 3.0) / r;
|
||||
let rr = (2.0 / 3.0 * v) / r;
|
||||
let ccy = -q - rr;
|
||||
var rx = round_away(q);
|
||||
let ry = round_away(ccy);
|
||||
var rz = round_away(rr);
|
||||
let ddx = abs(rx - q);
|
||||
let ddy = abs(ry - ccy);
|
||||
let ddz = abs(rz - rr);
|
||||
if (ddx > ddy && ddx > ddz) {
|
||||
rx = -ry - rz;
|
||||
} else if (ddy <= ddz) {
|
||||
rz = -rx - ry;
|
||||
}
|
||||
let hx = r * 1.7320508075688772 * (rx + rz / 2.0);
|
||||
let hy = r * 1.5 * rz;
|
||||
var lu = u - hx;
|
||||
var lv = v - hy;
|
||||
let odd = ((i32(rx) + i32(rz)) % 2 + 2) % 2 == 1;
|
||||
if ((mode == 1u || mode == 2u) && odd) {
|
||||
lu = -lu;
|
||||
}
|
||||
if (mode == 2u && odd) {
|
||||
lv = -lv;
|
||||
}
|
||||
let ox = lu * cs - lv * sn;
|
||||
let oy = lu * sn + lv * cs;
|
||||
let tx = cx + ox;
|
||||
let ty = cy + oy;
|
||||
let sx = tx + (f32(p.x) + 0.5 - tx) * smear;
|
||||
let sy = ty + (f32(p.y) + 0.5 - ty) * smear;
|
||||
textureStore(out, p, sample_bilinear_clamped(src, sx, sy));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Mr. Smoothie (stylize3::smoothie)
|
||||
// aux = flow plane (x). f[0] = (ax, ay, bx, by); f[1] = (phase, loops).
|
||||
|
||||
fn fp2_pal(i: f32) -> vec4<f32> {
|
||||
let t = i / 255.0;
|
||||
let a = P.f[0].xy;
|
||||
let b = P.f[0].zw;
|
||||
let s = sample_bilinear_clamped(src, a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t);
|
||||
return vec4<f32>(fp2_unpremul(s), 1.0);
|
||||
}
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fp2_smoothie(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fp2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let px = textureLoad(src, p, 0);
|
||||
if (px.w <= 0.0) {
|
||||
textureStore(out, p, px);
|
||||
return;
|
||||
}
|
||||
let v = textureLoad(aux, p, 0).x;
|
||||
var t = fract_euclid(v * P.f[1].y + P.f[1].x);
|
||||
if (t < 0.5) {
|
||||
t = t * 2.0;
|
||||
} else {
|
||||
t = 2.0 - t * 2.0;
|
||||
}
|
||||
let f = t * 255.0;
|
||||
let i = min(floor(f), 254.0);
|
||||
let a = fp2_pal(i);
|
||||
let b = fp2_pal(i + 1.0);
|
||||
let c = a + (b - a) * (f - i);
|
||||
textureStore(out, p, vec4<f32>(c.xyz * px.w, px.w));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Inner/Outer Key (keying2)
|
||||
|
||||
// T = (straight colour, trimap) from src = the layer and aux = the trimap (x).
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fp2_io_prep(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fp2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
textureStore(out, p, vec4<f32>(fp2_unpremul(textureLoad(src, p, 0)), textureLoad(aux, p, 0).x));
|
||||
}
|
||||
|
||||
// A known region (u[0].x: 0 foreground, 1 background) of T: (colour × known, known).
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fp2_io_known(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fp2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let t = textureLoad(src, p, 0);
|
||||
var m = select(0.0, 1.0, t.w > 0.75);
|
||||
if (P.u[0].x == 1u) {
|
||||
m = select(0.0, 1.0, t.w < 0.25);
|
||||
}
|
||||
textureStore(out, p, vec4<f32>(t.xyz * m, m));
|
||||
}
|
||||
|
||||
// src = blurred foreground, aux = blurred background, data = T's rows (row length u[0].x):
|
||||
// the estimated matte with the decontaminated colours (colour, alpha).
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fp2_io_est(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fp2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let at = (u32(p.y) * P.u[0].x + u32(p.x)) * 4u;
|
||||
let c = vec3<f32>(data[at], data[at + 1u], data[at + 2u]);
|
||||
let tri = data[at + 3u];
|
||||
var a = tri;
|
||||
if (!(tri > 0.25 && tri < 0.75)) {
|
||||
textureStore(out, p, vec4<f32>(c, a));
|
||||
return;
|
||||
}
|
||||
let fs = textureLoad(src, p, 0);
|
||||
let bs = textureLoad(aux, p, 0);
|
||||
let fc = fs.xyz / max(fs.w, 1e-6);
|
||||
let bc = bs.xyz / max(bs.w, 1e-6);
|
||||
let d = fc - bc;
|
||||
let dd = d.x * d.x + d.y * d.y + d.z * d.z;
|
||||
if (dd < 1e-6) {
|
||||
a = 0.5;
|
||||
} else {
|
||||
a = clamp(((c.x - bc.x) * d.x + (c.y - bc.y) * d.y + (c.z - bc.z) * d.z) / dd, 0.0, 1.0);
|
||||
}
|
||||
var col = c;
|
||||
if (a > 0.02) {
|
||||
col = clamp((c - (1.0 - a) * bc) / a, vec3<f32>(0.0), vec3<f32>(1.0));
|
||||
}
|
||||
textureStore(out, p, vec4<f32>(col, a));
|
||||
}
|
||||
|
||||
// The matte (src.x, or src.w with u[0].y) after the Cleanup strokes: data = targets (u[0].x of
|
||||
// them) then each stroke's coverage plane (negative = untouched).
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fp2_io_strokes(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fp2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let s = textureLoad(src, p, 0);
|
||||
var a = select(s.x, s.w, P.u[0].y != 0u);
|
||||
let n = P.u[0].x;
|
||||
let d = out_dims();
|
||||
let plane = u32(d.x * d.y);
|
||||
let i = u32(p.y * d.x + p.x);
|
||||
for (var k = 0u; k < n; k++) {
|
||||
let c = data[n + k * plane + i];
|
||||
if (c >= 0.0) {
|
||||
a += (data[k] - a) * c;
|
||||
}
|
||||
}
|
||||
textureStore(out, p, vec4<f32>(a, 0.0, 0.0, 0.0));
|
||||
}
|
||||
|
||||
// src = the layer, aux = the final matte (x), data = estimated colours' rows (row length u[0].x,
|
||||
// colours used when u[0].y). u[0].z = invert; f[0] = (threshold, blend with original).
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fp2_io_final(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fp2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let px = textureLoad(src, p, 0);
|
||||
var m = textureLoad(aux, p, 0).x;
|
||||
let thr = P.f[0].x;
|
||||
if (thr > 0.0 && m < thr) {
|
||||
m = 0.0;
|
||||
}
|
||||
if (P.u[0].z != 0u) {
|
||||
m = 1.0 - m;
|
||||
}
|
||||
var c = fp2_unpremul(px);
|
||||
if (P.u[0].y != 0u) {
|
||||
let at = (u32(p.y) * P.u[0].x + u32(p.x)) * 4u;
|
||||
c = vec3<f32>(data[at], data[at + 1u], data[at + 2u]);
|
||||
}
|
||||
let a = max(px.w, 0.0);
|
||||
let keyed = vec4<f32>(c * (a * m), a * m);
|
||||
textureStore(out, p, keyed + (px - keyed) * P.f[0].y);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Simple Wire Removal (keying2::wire_removal)
|
||||
// u[0].x = style; f[0] = (ax, ay, nx, ny); f[1] = (hw thickness, soft width, mirror).
|
||||
|
||||
fn fp2_wire_pos(base: vec2<f32>, nrm: vec2<f32>, s: f32) -> vec4<f32> {
|
||||
return sample_bilinear_clamped(src, base.x + nrm.x * s, base.y + nrm.y * s);
|
||||
}
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fp2_wire(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fp2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let px = textureLoad(src, p, 0);
|
||||
let a = P.f[0].xy;
|
||||
let nrm = P.f[0].zw;
|
||||
let hw = P.f[1].x;
|
||||
let soft_w = P.f[1].y;
|
||||
let mirror = P.f[1].z;
|
||||
let fx = f32(p.x) + 0.5;
|
||||
let fy = f32(p.y) + 0.5;
|
||||
let q = vec2<f32>(fx - a.x, fy - a.y);
|
||||
let d = q.x * nrm.x + q.y * nrm.y;
|
||||
let ad = abs(d);
|
||||
if (ad > hw + soft_w) {
|
||||
textureStore(out, p, px);
|
||||
return;
|
||||
}
|
||||
let base = vec2<f32>(fx - d * nrm.x, fy - d * nrm.y);
|
||||
let edge = hw + 1.0;
|
||||
var repl: vec4<f32>;
|
||||
switch P.u[0].x {
|
||||
case 0u: {
|
||||
let t = clamp((d + edge) / (2.0 * edge), 0.0, 1.0);
|
||||
let l = fp2_wire_pos(base, nrm, -edge);
|
||||
repl = l + (fp2_wire_pos(base, nrm, edge) - l) * t;
|
||||
}
|
||||
case 1u, 2u: {
|
||||
var s = d - 2.0 * (hw - ad) - 1.0;
|
||||
if (d >= 0.0) {
|
||||
s = d + 2.0 * (hw - ad) + 1.0;
|
||||
}
|
||||
let sh = fp2_wire_pos(base, nrm, s);
|
||||
repl = sh;
|
||||
if (mirror > 0.0) {
|
||||
repl = sh + (fp2_wire_pos(base, nrm, -s) - sh) * (mirror * 0.5);
|
||||
}
|
||||
}
|
||||
default: {
|
||||
var sx = 0.0;
|
||||
if (abs(nrm.x) > 1e-6) {
|
||||
sx = (hw - ad + 1.0) / abs(nrm.x);
|
||||
}
|
||||
let dir = select(-1.0, 1.0, d * nrm.x >= 0.0);
|
||||
let sh = sample_bilinear_clamped(src, fx + dir * sx, fy);
|
||||
let mi = sample_bilinear_clamped(src, fx - dir * sx, fy);
|
||||
repl = sh;
|
||||
if (mirror > 0.0) {
|
||||
repl = sh + (mi - sh) * (mirror * 0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
var k = 1.0;
|
||||
if (ad > hw) {
|
||||
k = 1.0 - fp2_smoothstep(hw, hw + soft_w, ad);
|
||||
}
|
||||
textureStore(out, p, px + (repl - px) * k);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Basic 3D (obsolete::basic_3d)
|
||||
// u[0] = (specular, wireframe); f[0] = (cx, cy, focal, distance); f[1] = ux; f[2] = uy;
|
||||
// f[3] = normal; f[4] = light; f[5] = (hw width, hw height).
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fp2_basic3d(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fp2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let cx = P.f[0].x;
|
||||
let cy = P.f[0].y;
|
||||
let f = P.f[0].z;
|
||||
let dist = P.f[0].w;
|
||||
let ux = P.f[1].xyz;
|
||||
let uy = P.f[2].xyz;
|
||||
let n = P.f[3].xyz;
|
||||
let d = vec3<f32>(f32(p.x) + 0.5 - cx, f32(p.y) + 0.5 - cy, f);
|
||||
let denom = dot(d, n);
|
||||
if (abs(denom) < 1e-9) {
|
||||
textureStore(out, p, vec4<f32>(0.0));
|
||||
return;
|
||||
}
|
||||
// Eye at (0, 0, -f), plane through (0, 0, dist).
|
||||
let t = (dist + f) * n.z / denom;
|
||||
if (t <= 0.0) {
|
||||
textureStore(out, p, vec4<f32>(0.0));
|
||||
return;
|
||||
}
|
||||
let hit = vec3<f32>(d.x * t, d.y * t, -f + d.z * t - dist);
|
||||
let u = dot(hit, ux);
|
||||
let v = dot(hit, uy);
|
||||
var c = sample_bilinear(src, cx + u, cy + v);
|
||||
if (P.u[0].x != 0u && c.w > 0.0) {
|
||||
let dv = d / sqrt(dot(d, d));
|
||||
let k = 2.0 * dot(dv, n);
|
||||
let r = dv - k * n;
|
||||
let s = powz(max(-dot(r, P.f[4].xyz), 0.0), 40.0);
|
||||
let a = c.w;
|
||||
c = vec4<f32>(c.xyz + s * a * max(vec3<f32>(1.0) - c.xyz / max(a, 1e-6), vec3<f32>(0.0)), a);
|
||||
}
|
||||
if (P.u[0].y != 0u) {
|
||||
let hw = P.f[5].x;
|
||||
let hh = P.f[5].y;
|
||||
let e = min(abs(abs(u) - hw), abs(abs(v) - hh));
|
||||
if (e < 0.75 && abs(u) <= hw + 0.75 && abs(v) <= hh + 0.75) {
|
||||
c = vec4<f32>(1.0);
|
||||
}
|
||||
}
|
||||
textureStore(out, p, c);
|
||||
}
|
||||
|
|
@ -57,7 +57,7 @@ fn gpu_adjustment_chains_match_the_cpu() {
|
|||
#[test]
|
||||
fn mixed_adjustment_chains_fall_back_per_effect() {
|
||||
let mut stack = gpu_stack();
|
||||
stack.insert(1, ("ec.stylize.ccplastic", vec![]));
|
||||
stack.insert(1, ("ec.distort.rollingshutterrepair", vec![]));
|
||||
stack.push(("ec.stylize.glow", vec![("threshold", n(30.0))]));
|
||||
for depth in [BitDepth::Bpc8, BitDepth::Bpc32] {
|
||||
let s = adjustment_scene(depth, &stack, true);
|
||||
|
|
@ -93,7 +93,7 @@ impl FxTarget for Probe {
|
|||
fn adjustment_stacks_split_into_gpu_runs_and_cpu_steps() {
|
||||
let Some(g) = gpu() else { return };
|
||||
let mut stack = gpu_stack();
|
||||
stack.insert(1, ("ec.stylize.ccplastic", vec![]));
|
||||
stack.insert(1, ("ec.generate.fractal", vec![]));
|
||||
let s = adjustment_scene(BitDepth::Bpc32, &stack, false);
|
||||
let mut r = Renderer::new(&s.p, &Pattern, RenderOpts { backend: Backend::Gpu, ..opts() });
|
||||
r.accel = Some(g);
|
||||
|
|
|
|||
|
|
@ -466,7 +466,7 @@ fn gpu_compositor_draws_split_advanced_3d_runs_and_skies() {
|
|||
// A varying environment image (so the sky's lookup shows; a CPU effect, so the frame's
|
||||
// only readback is its own).
|
||||
let mut next = p.next_id;
|
||||
let spec = effectcraft_effects::find("ec.generate.lensflare").unwrap();
|
||||
let spec = effectcraft_effects::find("ec.generate.fractal").unwrap();
|
||||
let ramp = effectcraft_effects::instantiate(spec, &mut build::Ids(&mut next), spec.name, [32.0, 16.0]);
|
||||
p.next_id = next;
|
||||
if case == 3 {
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ fn deferred_gpu() -> Option<(&'static Gpu, std::sync::MutexGuard<'static, ()>)>
|
|||
/// Footage with GPU effects around a CPU-only one, a solid with Glow, and a precomp whose
|
||||
/// layer is blurred inside and outside (a chain whose input is another chain's result).
|
||||
fn scene() -> Scene {
|
||||
let cpu_only = "ec.blur.ccradial";
|
||||
let cpu_only = "ec.generate.fractal";
|
||||
assert!(!effectcraft_effects::GPU_EFFECTS.contains(&cpu_only));
|
||||
let mut s = Scene::new(BitDepth::Bpc32);
|
||||
let mut bg = s.footage(97, 61);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,176 @@
|
|||
//! GPU generators of part C (Lightning, Advanced Lightning, Beam, Lens Flare, Radio
|
||||
//! Waves, Vegas, Stroke, Scribble, Write-on, Paint Bucket, Eyedropper Fill, CC Glue Gun, CC
|
||||
//! Threads, Audio Spectrum, Audio Waveform, Basic Text, Path Text) vs the CPU effects (the
|
||||
//! oracle): direct on a buffer with masks, another layer and audio at full and half resolution
|
||||
//! and as adjustment, and composited at 8 and 32 bpc.
|
||||
|
||||
use effectcraft_keyframe::Value;
|
||||
use effectcraft_time::Tick;
|
||||
|
||||
use crate::tests::{c, n};
|
||||
use crate::tests_fx_pixel2::{e, env_case, env_case_allow, env_case_at, off, on, pt};
|
||||
|
||||
fn layer(id: u64) -> Value {
|
||||
Value::Layer(Some(id))
|
||||
}
|
||||
|
||||
fn text(s: &str) -> Value {
|
||||
Value::Str(s.into())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lightning() {
|
||||
env_case("ec.obsolete.lightning", &[]);
|
||||
env_case(
|
||||
"ec.obsolete.lightning",
|
||||
&[("branching", n(0.9)), ("rebranching", n(0.6)), ("detailLevel", n(4.0)), ("blendingMode", e(1)), ("pullForce", n(40.0))],
|
||||
);
|
||||
env_case("ec.obsolete.lightning", &[("blendingMode", e(2)), ("rerunAtEachFrame", on()), ("fixedEndpoint", on()), ("coreWidth", n(0.8))]);
|
||||
env_case("ec.generate.advancedlightning", &[]);
|
||||
env_case("ec.generate.advancedlightning", &[("lightningType", e(4)), ("forking", n(60.0)), ("decayMainCore", on()), ("expertSettings/coreDrain", n(40.0))]);
|
||||
env_case(
|
||||
"ec.generate.advancedlightning",
|
||||
&[("alphaObstacle", n(6.0)), ("compositeOnOriginal", off()), ("expertSettings/fractalType", e(2)), ("glowSettings/glowRadius", n(10.0))],
|
||||
);
|
||||
env_case("ec.generate.advancedlightning", &[("lightningType", e(7)), ("glowSettings/glowRadius", n(0.0)), ("coreSettings/coreRadius", n(1.5))]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn closed_form() {
|
||||
env_case("ec.generate.beam", &[]);
|
||||
env_case("ec.generate.beam", &[("length", n(70.0)), ("time", n(40.0)), ("softness", n(0.0)), ("perspective3d", off()), ("compositeOnOriginal", off())]);
|
||||
env_case("ec.generate.beam", &[("startThickness", n(3.0)), ("endThickness", n(20.0)), ("insideColor", c(0.3, 0.9, 1.0))]);
|
||||
env_case("ec.generate.lensflare", &[]);
|
||||
env_case("ec.generate.lensflare", &[("lensType", e(1)), ("flareCenter", pt(50.0, 30.0)), ("blendWithOriginal", n(30.0))]);
|
||||
env_case("ec.generate.lensflare", &[("lensType", e(2)), ("flareBrightness", n(160.0))]);
|
||||
env_case("ec.generate.ccgluegun", &[]);
|
||||
env_case("ec.generate.ccgluegun", &[("density", n(12.0)), ("strokeWidth", n(40.0)), ("style", e(1)), ("strength", n(60.0))]);
|
||||
let t = |vals: &[(&str, Value)]| env_case_allow("ec.generate.ccthreads", vals, THREAD_TIES);
|
||||
t(&[]);
|
||||
t(&[("texture", n(0.0))]);
|
||||
// Generic values: no ties.
|
||||
env_case("ec.generate.ccthreads", &[("width", n(13.1)), ("height", n(17.3)), ("overlaps", n(2.0)), ("direction", n(23.0)), ("texture", n(0.0))]);
|
||||
env_case(
|
||||
"ec.generate.ccthreads",
|
||||
&[("width", n(9.3)), ("height", n(11.1)), ("coverage", n(55.3)), ("shadowing", n(90.0)), ("texture", n(40.0)), ("center", pt(33.7, 21.9))],
|
||||
);
|
||||
}
|
||||
|
||||
/// CC Threads decides per pixel which thread band it lies in and which fibre (the position
|
||||
/// across / along the thread times 8 / 64, truncated) shades it: at parameter values that put
|
||||
/// pixel centres exactly on a band edge or fibre boundary (the defaults do), f32 and the CPU's
|
||||
/// f64 can land on either side.
|
||||
const THREAD_TIES: f64 = 0.05;
|
||||
|
||||
#[test]
|
||||
fn radio_waves() {
|
||||
let t = Tick::from_seconds_f64(1.3);
|
||||
env_case_at("ec.generate.radiowaves", &[("waveMotion/expansion", n(30.0))], 0.0, t);
|
||||
env_case_at(
|
||||
"ec.generate.radiowaves",
|
||||
&[
|
||||
("polygon/sides", n(5.0)),
|
||||
("polygon/star", on()),
|
||||
("waveMotion/spin", n(30.0)),
|
||||
("waveStroke/profile", e(4)),
|
||||
("waveMotion/velocity", n(20.0)),
|
||||
("waveMotion/expansion", n(25.0)),
|
||||
],
|
||||
0.0,
|
||||
t,
|
||||
);
|
||||
env_case_at(
|
||||
"ec.generate.radiowaves",
|
||||
&[("waveType", e(1)), ("imageContour/sourceLayer", layer(9)), ("waveStroke/fadeOutTime", n(1.0)), ("waveMotion/expansion", n(10.0))],
|
||||
0.0,
|
||||
t,
|
||||
);
|
||||
env_case_at(
|
||||
"ec.generate.radiowaves",
|
||||
&[
|
||||
("waveType", e(2)),
|
||||
("waveMask/mask", n(1.0)),
|
||||
("waveMotion/reflection", on()),
|
||||
("waveMotion/velocity", n(40.0)),
|
||||
("waveMotion/expansion", n(15.0)),
|
||||
("waveStroke/profile", e(5)),
|
||||
],
|
||||
0.0,
|
||||
t,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strokes() {
|
||||
env_case("ec.generate.stroke", &[]);
|
||||
env_case("ec.generate.stroke", &[("allMasks", on()), ("spacing", n(80.0)), ("paintStyle", e(1)), ("end", n(70.0)), ("brushSize", n(6.0))]);
|
||||
env_case("ec.generate.stroke", &[("path", n(3.0)), ("paintStyle", e(2)), ("start", n(20.0)), ("brushHardness", n(10.0))]);
|
||||
env_case("ec.generate.scribble", &[]);
|
||||
env_case("ec.generate.scribble", &[("scribble", e(1)), ("fillType", e(2)), ("edgeOptions/endCap", e(2)), ("edgeOptions/join", e(1)), ("composite", e(0))]);
|
||||
env_case(
|
||||
"ec.generate.scribble",
|
||||
&[("mask", n(2.0)), ("fillType", e(1)), ("edgeOptions/join", e(2)), ("wiggleType", e(2)), ("end", n(60.0)), ("composite", e(2))],
|
||||
);
|
||||
env_case("ec.generate.scribble", &[("mask", n(3.0))]);
|
||||
env_case("ec.generate.vegas", &[]);
|
||||
env_case("ec.generate.vegas", &[("stroke", e(1)), ("path", n(3.0)), ("blendMode", e(2)), ("segments", n(12.0)), ("randomPhase", on())]);
|
||||
env_case("ec.generate.vegas", &[("inputLayer", layer(9)), ("blendMode", e(3)), ("shorterContoursHave", e(1)), ("threshold", n(30.0))]);
|
||||
env_case("ec.generate.vegas", &[("stroke", e(1)), ("path", n(1.0)), ("blendMode", e(0)), ("width", n(5.0)), ("hardness", n(0.9))]);
|
||||
env_case("ec.generate.writeon", &[]);
|
||||
env_case("ec.generate.writeon", &[("brushPosition", pt(20.0, 30.0)), ("brushSize", n(14.0)), ("paintStyle", e(1))]);
|
||||
env_case("ec.generate.writeon", &[("paintStyle", e(2)), ("brushHardness", n(20.0)), ("brushOpacity", n(60.0))]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fills() {
|
||||
env_case("ec.generate.paintbucket", &[]);
|
||||
env_case(
|
||||
"ec.generate.paintbucket",
|
||||
&[("fillPoint", pt(10.0, 10.0)), ("fillSelector", e(1)), ("tolerance", n(60.0)), ("stroke", e(4)), ("strokeWidth", n(4.0))],
|
||||
);
|
||||
env_case("ec.generate.paintbucket", &[("fillPoint", pt(35.0, 22.0)), ("fillSelector", e(2)), ("stroke", e(1)), ("color", c(0.2, 0.9, 0.4))]);
|
||||
env_case("ec.generate.paintbucket", &[("stroke", e(2)), ("spreadRadius", n(2.5)), ("tolerance", n(120.0)), ("blendingMode", e(5))]);
|
||||
env_case("ec.generate.paintbucket", &[("stroke", e(3)), ("tolerance", n(90.0)), ("blendingMode", e(9))]);
|
||||
env_case("ec.generate.paintbucket", &[("viewThreshold", on()), ("tolerance", n(80.0))]);
|
||||
env_case("ec.generate.paintbucket", &[("tolerance", n(100.0)), ("blendingMode", e(8)), ("opacity", n(70.0))]);
|
||||
env_case("ec.generate.eyedropperfill", &[]);
|
||||
env_case(
|
||||
"ec.generate.eyedropperfill",
|
||||
&[("samplePoint", pt(12.0, 30.0)), ("sampleRadius", n(6.0)), ("averagePixelColors", e(3)), ("blendWithOriginal", n(30.0))],
|
||||
);
|
||||
env_case("ec.generate.eyedropperfill", &[("sampleRadius", n(9.0)), ("averagePixelColors", e(1)), ("maintainOriginalAlpha", on())]);
|
||||
env_case("ec.generate.eyedropperfill", &[("sampleRadius", n(4.0)), ("averagePixelColors", e(2))]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn audio() {
|
||||
let a = || ("audioLayer", layer(9));
|
||||
env_case("ec.generate.audiospectrum", &[a()]);
|
||||
env_case("ec.generate.audiospectrum", &[a(), ("displayOptions", e(1)), ("sideOptions", e(2)), ("compositeOnOriginal", on())]);
|
||||
env_case("ec.generate.audiospectrum", &[a(), ("displayOptions", e(2)), ("path", n(3.0)), ("softness", n(0.0))]);
|
||||
env_case("ec.generate.audiowaveform", &[a()]);
|
||||
env_case("ec.generate.audiowaveform", &[a(), ("displayOptions", e(0)), ("waveformOptions", e(2)), ("usePolarPath", on())]);
|
||||
env_case("ec.generate.audiowaveform", &[a(), ("displayOptions", e(2)), ("path", n(1.0)), ("compositeOnOriginal", on())]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_effects() {
|
||||
env_case("ec.obsolete.basictext", &[]);
|
||||
env_case("ec.obsolete.basictext", &[("text", text("Hi\nthere")), ("fillAndStroke/displayOptions", e(2)), ("alignment", e(0)), ("size", n(14.0))]);
|
||||
env_case("ec.obsolete.basictext", &[("fillAndStroke/displayOptions", e(3)), ("fillAndStroke/strokeWidth", n(5.0)), ("compositeOnOriginal", on())]);
|
||||
env_case("ec.obsolete.basictext", &[("fillAndStroke/displayOptions", e(1)), ("fillAndStroke/strokeWidth", n(1.0)), ("size", n(20.0))]);
|
||||
env_case("ec.obsolete.pathtext", &[]);
|
||||
env_case(
|
||||
"ec.obsolete.pathtext",
|
||||
&[("pathOptions/shapeType", e(1)), ("advanced/visibleCharacters", n(2.5)), ("advanced/fadeTime", n(50.0)), ("character/size", n(14.0))],
|
||||
);
|
||||
env_case(
|
||||
"ec.obsolete.pathtext",
|
||||
&[
|
||||
("pathOptions/customPath", n(3.0)),
|
||||
("fillAndStroke/displayOptions", e(2)),
|
||||
("advanced/jitterSettings/rotationJitterMax", n(20.0)),
|
||||
("compositeOnOriginal", on()),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,250 @@
|
|||
//! GPU pixel ports of part C (CC Cross / Radial / Vector Blur, Reduce Interlace Flicker, CC
|
||||
//! Composite, Color Link, Cineon Converter, HDR Compander, HDR Highlight Compression, Grow
|
||||
//! Bounds, CC Overbrights, CC Block Load, CC Burn Film, CC Glass, CC HexTile, CC Mr. Smoothie,
|
||||
//! CC Plastic, Inner/Outer Key, CC Simple Wire Removal, Basic 3D) vs the CPU effects (the
|
||||
//! oracle): direct on a buffer at full and half resolution, as adjustment, and composited at 8
|
||||
//! and 32 bpc. Also the helpers for effects that read masks, other layers and audio.
|
||||
|
||||
use effectcraft_effects::{Buf, EffectEnv, EffectHost, LayerPixels, MaskShape};
|
||||
use effectcraft_keyframe::{ShapePath, Value};
|
||||
use effectcraft_project::build::{self, Ids};
|
||||
use effectcraft_project::{BitDepth, MaskMode};
|
||||
use effectcraft_render::RenderOpts;
|
||||
use effectcraft_time::Tick;
|
||||
|
||||
use crate::tests::{Scene, c, check, compare_at, diff, effect_case, gpu, n, opts, pattern, set};
|
||||
|
||||
pub(crate) fn e(v: u32) -> Value {
|
||||
Value::Enum(v)
|
||||
}
|
||||
|
||||
pub(crate) fn on() -> Value {
|
||||
Value::Bool(true)
|
||||
}
|
||||
|
||||
pub(crate) fn off() -> Value {
|
||||
Value::Bool(false)
|
||||
}
|
||||
|
||||
pub(crate) fn pt(x: f64, y: f64) -> Value {
|
||||
Value::Vec2([x, y])
|
||||
}
|
||||
|
||||
/// Size of the test layer (layer pixels).
|
||||
const SIZE: [f64; 2] = [70.0, 44.0];
|
||||
|
||||
/// The test layer's masks (layer pixels): a closed pentagon, a closed quad and an open curve.
|
||||
pub(crate) fn mask_points() -> Vec<(Vec<[f64; 2]>, bool)> {
|
||||
let pent = (0..5)
|
||||
.map(|i| {
|
||||
let a = i as f64 * std::f64::consts::TAU / 5.0 - 1.3;
|
||||
[33.3 + a.cos() * 17.1, 22.4 + a.sin() * 15.2]
|
||||
})
|
||||
.collect();
|
||||
vec![
|
||||
(pent, true),
|
||||
(vec![[6.3, 5.1], [62.2, 7.7], [59.4, 39.6], [9.1, 37.2]], true),
|
||||
(vec![[4.2, 30.3], [18.7, 12.4], [33.1, 28.9], [47.6, 9.8], [64.2, 25.5]], false),
|
||||
]
|
||||
}
|
||||
|
||||
/// Another layer (`id` 9) and an audio track for layer parameters.
|
||||
pub(crate) struct Host;
|
||||
impl EffectHost for Host {
|
||||
fn layer(&self, id: u64, _: bool) -> Option<LayerPixels> {
|
||||
Some(LayerPixels { buf: Buf { img: pattern(id as u32 + 3, 70, 44), offset: [0.0; 2], scale: 1.0 }, size: SIZE })
|
||||
}
|
||||
fn audio(&self, _: u64, start: f64, frames: usize, rate: u32) -> Option<Vec<f32>> {
|
||||
Some(
|
||||
(0..frames)
|
||||
.flat_map(|i| {
|
||||
let t = start + i as f64 / rate as f64;
|
||||
let l = 0.5 * (t * 440.0 * std::f64::consts::TAU).sin() + 0.2 * (t * 1300.0 * std::f64::consts::TAU).sin();
|
||||
let r = 0.4 * (t * (200.0 + 300.0 * t) * std::f64::consts::TAU).sin();
|
||||
[l as f32, r as f32]
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// The effect alone on a buffer with the test masks and [`Host`]: GPU vs CPU (32-bit float)
|
||||
/// at full and half resolution (the half buffer padded and offset), direct and as adjustment.
|
||||
/// `allow` = fraction of pixels allowed over 1e-3.
|
||||
pub(crate) fn direct_env(id: &str, vals: &[(&str, Value)], allow: f64) {
|
||||
let Some(g) = gpu() else { return };
|
||||
let spec = effectcraft_effects::find(id).unwrap();
|
||||
let mut params =
|
||||
effectcraft_effects::Params { values: spec.params.iter().map(|ps| (ps.id.to_string(), effectcraft_effects::default_value(ps, SIZE))).collect() };
|
||||
for (k, v) in vals {
|
||||
assert!(params.values.contains_key(*k), "{id}: no {k}");
|
||||
params.values.insert(k.to_string(), v.clone());
|
||||
}
|
||||
let masks: Vec<MaskShape> = mask_points()
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, (points, closed))| MaskShape { name: format!("Mask {}", i + 1), points, closed, inverted: false })
|
||||
.collect();
|
||||
let host = Host;
|
||||
for adjustment in [false, true] {
|
||||
for scale in [1.0, 0.5] {
|
||||
let img = pattern(7, (SIZE[0] * scale) as u32, (SIZE[1] * scale) as u32);
|
||||
let mut buf = Buf { img, offset: [0.0, 0.0], scale };
|
||||
if scale < 1.0 {
|
||||
buf.pad(3);
|
||||
}
|
||||
let env = EffectEnv { masks: &masks, host: Some(&host), comp_time: 0.25, frame_rate: 30.0, ..Default::default() };
|
||||
let ctx = || effectcraft_effects::EffectCtx { params: ¶ms, time: 0.25, layer_size: SIZE, seed: 11, adjustment, env };
|
||||
let cpu = (spec.render)(&ctx(), buf.clone());
|
||||
let out =
|
||||
effectcraft_render::Accelerator::effects(g, &[effectcraft_render::FxStep { spec, ctx: ctx() }], &buf, None).expect("the GPU runs the effect");
|
||||
assert_eq!((out.offset, out.scale), (cpu.offset, cpu.scale), "{id}: geometry");
|
||||
let d = diff(&cpu.img, &out.img, 1e-3);
|
||||
assert!(
|
||||
d.over as f64 <= allow * d.total as f64,
|
||||
"{id} {vals:?} adj {adjustment} scale {scale}: {} of {} pixels over 1e-3 (max {}); worst {:?}",
|
||||
d.over,
|
||||
d.total,
|
||||
d.max,
|
||||
d.worst
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// [`direct_env`], then a footage layer with the test masks (mode None) and the effect,
|
||||
/// composited over a background at 8 and 32 bpc, full and half resolution.
|
||||
pub(crate) fn env_case(id: &str, vals: &[(&str, Value)]) {
|
||||
env_case_allow(id, vals, 0.0);
|
||||
}
|
||||
|
||||
/// [`env_case`] allowing a fraction of pixels over the tolerance (documented exceptions).
|
||||
pub(crate) fn env_case_allow(id: &str, vals: &[(&str, Value)], allow: f64) {
|
||||
env_case_at(id, vals, allow, Tick::ZERO);
|
||||
}
|
||||
|
||||
/// [`env_case_allow`] with the composited frames at comp time `t`.
|
||||
pub(crate) fn env_case_at(id: &str, vals: &[(&str, Value)], allow: f64, t: Tick) {
|
||||
direct_env(id, vals, allow);
|
||||
for depth in [BitDepth::Bpc8, BitDepth::Bpc32] {
|
||||
let mut s = Scene::new(depth);
|
||||
let bg = s.solid([0.15, 0.1, 0.2], 97, 61);
|
||||
s.push(bg);
|
||||
let mut l = s.footage(70, 44);
|
||||
let mut next = s.p.next_id;
|
||||
for (i, (pts, closed)) in mask_points().into_iter().enumerate() {
|
||||
let m = build::mask(&mut Ids(&mut next), &format!("Mask {}", i + 1), ShapePath::polygon(&pts, closed), MaskMode::None, [255, 255, 0]);
|
||||
l.props.sub_mut("masks").unwrap().children.push(m.into());
|
||||
}
|
||||
s.p.next_id = next;
|
||||
s.effect(&mut l, id, vals);
|
||||
set(&mut l, "transform/rotation", Value::Scalar(8.0));
|
||||
s.push(l);
|
||||
check(&format!("{id} {vals:?} {depth:?}"), compare_at(&s, opts(), t), allow);
|
||||
check(&format!("{id} {vals:?} {depth:?} half"), compare_at(&s, RenderOpts { scale: 0.5, ..opts() }, t), allow);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blurs() {
|
||||
effect_case("ec.blur.cccross", &[]);
|
||||
effect_case("ec.blur.cccross", &[("radiusX", n(4.0)), ("radiusY", n(0.0)), ("transferMode", e(1))]);
|
||||
effect_case("ec.blur.cccross", &[("radiusX", n(3.0)), ("radiusY", n(7.0)), ("transferMode", e(2))]);
|
||||
effect_case("ec.blur.cccross", &[("radiusX", n(6.0)), ("radiusY", n(2.0)), ("transferMode", e(3))]);
|
||||
effect_case("ec.blur.ccradial", &[]);
|
||||
effect_case("ec.blur.ccradial", &[("type", e(0)), ("amount", n(-40.0)), ("center", pt(20.0, 30.0))]);
|
||||
effect_case("ec.blur.ccradial", &[("type", e(2)), ("amount", n(60.0)), ("quality", n(20.0))]);
|
||||
effect_case("ec.blur.ccradial", &[("type", e(3)), ("amount", n(30.0))]);
|
||||
effect_case("ec.blur.ccradial", &[("type", e(4)), ("amount", n(-50.0)), ("quality", n(80.0))]);
|
||||
effect_case("ec.blur.ccvector", &[]);
|
||||
effect_case("ec.blur.ccvector", &[("type", e(1)), ("amount", n(6.0)), ("angleOffset", n(30.0))]);
|
||||
effect_case("ec.blur.ccvector", &[("type", e(2)), ("mapSoftness", n(0.0))]);
|
||||
effect_case("ec.blur.ccvector", &[("type", e(3)), ("amount", n(5.0))]);
|
||||
effect_case("ec.blur.ccvector", &[("type", e(4)), ("amount", n(12.0)), ("mapSoftness", n(40.0))]);
|
||||
effect_case("ec.blur.reduceflicker", &[("softness", n(3.0))]);
|
||||
effect_case("ec.blur.reduceflicker", &[("softness", n(0.5))]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn composites_and_colour() {
|
||||
for m in [0, 1, 2, 3, 5, 8, 11, 14, 15] {
|
||||
effect_case("ec.channel.cccomposite", &[("compositeOriginal", e(m)), ("opacity", n(70.0))]);
|
||||
}
|
||||
effect_case("ec.channel.cccomposite", &[("compositeOriginal", e(4)), ("rgbOnly", on())]);
|
||||
effect_case("ec.color.colorlink", &[]);
|
||||
effect_case("ec.color.colorlink", &[("sampleSource", e(2)), ("clip", n(10.0)), ("blendingMode", e(3)), ("opacity", n(60.0))]);
|
||||
effect_case("ec.color.colorlink", &[("sampleSource", e(1)), ("blendingMode", e(11)), ("stencilOriginalAlpha", off())]);
|
||||
effect_case("ec.color.colorlink", &[("sampleSource", e(9)), ("clip", n(20.0))]);
|
||||
effect_case("ec.color.colorlink", &[("sampleSource", e(6)), ("opacity", n(50.0)), ("stencilOriginalAlpha", off())]);
|
||||
env_case("ec.color.colorlink", &[("sourceLayer", Value::Layer(Some(9))), ("sampleSource", e(4)), ("clip", n(5.0))]);
|
||||
effect_case("ec.utility.cineon", &[]);
|
||||
effect_case("ec.utility.cineon", &[("conversionType", e(0)), ("gamma", n(2.2))]);
|
||||
effect_case("ec.utility.cineon", &[("conversionType", e(2)), ("highlightRolloff", n(60.0)), ("tenBitWhitePoint", n(800.0))]);
|
||||
effect_case("ec.utility.hdrcompander", &[("gain", n(4.0)), ("gamma", n(2.0))]);
|
||||
effect_case("ec.utility.hdrcompander", &[("mode", e(1)), ("gain", n(3.0)), ("gamma", n(1.5))]);
|
||||
effect_case("ec.utility.hdrcompression", &[]);
|
||||
effect_case("ec.utility.hdrcompression", &[("amount", n(40.0))]);
|
||||
effect_case("ec.utility.growbounds", &[("pixels", n(9.0))]);
|
||||
effect_case("ec.utility.ccoverbrights", &[]);
|
||||
for ch in 1..=5 {
|
||||
effect_case("ec.utility.ccoverbrights", &[("channel", e(ch)), ("highlightColor", c(0.2, 0.9, 0.4))]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stylize() {
|
||||
for (done, flags) in
|
||||
[(30.0, [true, true, true, false]), (55.0, [false, true, false, true]), (70.0, [true, false, false, false]), (10.0, [false, false, true, true])]
|
||||
{
|
||||
effect_case(
|
||||
"ec.stylize.ccblockload",
|
||||
&[
|
||||
("completion", n(done)),
|
||||
("scanlines", Value::Bool(flags[0])),
|
||||
("smoothing", Value::Bool(flags[1])),
|
||||
("startCleared", Value::Bool(flags[2])),
|
||||
("bilinear", Value::Bool(flags[3])),
|
||||
],
|
||||
);
|
||||
}
|
||||
effect_case("ec.stylize.ccburnfilm", &[("burn", n(30.0))]);
|
||||
effect_case("ec.stylize.ccburnfilm", &[("burn", n(60.0)), ("center", pt(20.0, 15.0)), ("randomSeed", n(4.0))]);
|
||||
effect_case("ec.stylize.ccglass", &[]);
|
||||
effect_case("ec.stylize.ccglass", &[("lightType", e(1)), ("displacement", n(-60.0)), ("property", e(4)), ("softness", n(4.0))]);
|
||||
effect_case("ec.stylize.ccglass", &[("height", n(0.0)), ("lightColor", c(1.0, 0.8, 0.5)), ("metal", n(60.0))]);
|
||||
effect_case("ec.stylize.ccplastic", &[]);
|
||||
effect_case("ec.stylize.ccplastic", &[("lightType", e(1)), ("cutMin", n(20.0)), ("cutMax", n(70.0)), ("property", e(5))]);
|
||||
env_case("ec.stylize.ccplastic", &[("bumpLayer", Value::Layer(Some(9))), ("property", e(0)), ("roughness", n(0.2))]);
|
||||
effect_case("ec.stylize.cchextile", &[("radius", n(9.3))]);
|
||||
effect_case("ec.stylize.cchextile", &[("radius", n(7.7)), ("render", e(1)), ("rotate", n(17.0)), ("smearing", n(30.0))]);
|
||||
effect_case("ec.stylize.cchextile", &[("radius", n(12.1)), ("render", e(2)), ("center", pt(31.3, 19.7))]);
|
||||
effect_case("ec.stylize.ccmrsmoothie", &[]);
|
||||
effect_case("ec.stylize.ccmrsmoothie", &[("property", e(6)), ("smoothness", n(5.0)), ("colorLoop", e(2)), ("phase", n(40.0))]);
|
||||
env_case("ec.stylize.ccmrsmoothie", &[("flowLayer", Value::Layer(Some(9))), ("property", e(1))]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keys_and_3d() {
|
||||
env_case("ec.key.innerouter", &[]);
|
||||
env_case("ec.key.innerouter", &[("foreground", e(1))]);
|
||||
direct_env("ec.key.innerouter", &[("foreground", e(1)), ("background", e(2))], 0.0);
|
||||
env_case("ec.key.innerouter", &[("foreground", e(1)), ("background", e(2)), ("edgeFeather", n(3.0)), ("edgeThin", n(1.5))]);
|
||||
env_case("ec.key.innerouter", &[("foreground", e(1)), ("singleMaskHighlightRadius", n(4.0)), ("invertExtraction", on()), ("blendWithOriginal", n(20.0))]);
|
||||
env_case(
|
||||
"ec.key.innerouter",
|
||||
&[
|
||||
("foreground", e(2)),
|
||||
("edgeThin", n(-2.3)),
|
||||
("edgeThreshold", n(30.0)),
|
||||
("cleanupForeground/cleanupForeground1/path", e(3)),
|
||||
("cleanupBackground/cleanupBackground1/path", e(1)),
|
||||
],
|
||||
);
|
||||
effect_case("ec.key.ccsimplewireremoval", &[]);
|
||||
effect_case("ec.key.ccsimplewireremoval", &[("removalStyle", e(0)), ("thickness", n(8.0)), ("slope", n(50.0))]);
|
||||
effect_case("ec.key.ccsimplewireremoval", &[("removalStyle", e(1)), ("mirrorBlend", n(60.0)), ("pointB", pt(60.0, 10.0))]);
|
||||
effect_case("ec.key.ccsimplewireremoval", &[("removalStyle", e(3)), ("mirrorBlend", n(30.0)), ("slope", n(20.0))]);
|
||||
effect_case("ec.obsolete.basic3d", &[("swivel", n(25.0))]);
|
||||
effect_case("ec.obsolete.basic3d", &[("tilt", n(-30.0)), ("swivel", n(10.0)), ("specularHighlight", on())]);
|
||||
effect_case("ec.obsolete.basic3d", &[("distanceToImage", n(20.0)), ("preview/drawPreviewWireframe", on()), ("tilt", n(15.0))]);
|
||||
}
|
||||
|
|
@ -209,7 +209,7 @@ Preserve Transparency / blend kernels (a 3D track matte renders solo through the
|
|||
Environment Light Background layers draw their sky in a kernel (`adv_sky`, from
|
||||
`Renderer::sky_draw`) before the rest of the run, in Classic and Advanced 3D comps. Wireframe
|
||||
outlines draw on the GPU from the CPU's pixel list (`Renderer::wireframe_pixels`). GPU effects
|
||||
(`effects::GPU_EFFECTS`, 243 of them: blurs, colour correction, keying incl. Key Light, mattes,
|
||||
(`effects::GPU_EFFECTS`, 280 of them: blurs, colour correction, keying incl. Key Light, mattes,
|
||||
channel, stylize, distortion and warps (Warp, Bezier Warp, Smear, Reshape, CC Bend It, CC Page
|
||||
Turn, CC Bender, CC Blobbylize), Cartoon, bevels, shapes, the transitions (CC light family, CC
|
||||
transitions, Block Dissolve), perspective (Radial Shadow, CC Cylinder / Sphere / Spotlight /
|
||||
|
|
@ -219,8 +219,11 @@ extra texture), Immersive Video, colour management (colour programs from
|
|||
simulation stays on the CPU and hands its plan — sprites, pieces, blobs, grids — to a tiled
|
||||
rasteriser; the particle effects too, `gpu::fx_particles`: CC Particle World / Systems II,
|
||||
Particle Playground with its Layer Map frames in one atlas, CC Ball Action, CC Pixel Polly, CC
|
||||
Scatterize; plans too large for one item table draw in several passes over the previous one);
|
||||
see
|
||||
Scatterize; plans too large for one item table draw in several passes over the previous one),
|
||||
and the geometry generators (Lightning, Advanced Lightning, Radio Waves, Stroke, Scribble,
|
||||
Vegas, Write-on, Audio Spectrum / Waveform, Basic / Path Text: bolts, strokes, waves, audio
|
||||
marks and glyphs are planned on the CPU, shared with the CPU effect, and rasterised in tiles
|
||||
with the CPU's coverage functions); see
|
||||
[effects.md](effects.md)) repeat the CPU effect's steps (padding, box radii, parameters, hashes)
|
||||
as kernels, in one module per family (`gpu::fx_*` with `shaders/fx_*.wgsl`); consecutive GPU
|
||||
effects run as one chain with one upload and one readback. Statistics that need the whole frame
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
EffectCraft ships 306 effects with 3009 parameters in total, grouped into the same categories as the Effects & Presets panel. Every effect is our own implementation, written from public behaviour descriptions and standard image-processing literature. Parameter names, order, popup options, units, defaults and ranges follow the reference application so that projects, expressions and muscle memory carry over.
|
||||
|
||||
- **GPU**: 243 effects also run on the GPU compositor with identical results.
|
||||
- **GPU**: 280 effects also run on the GPU compositor with identical results.
|
||||
- **32**: 306 effects process 32-bit float (HDR, overbright) pixels without clamping.
|
||||
- **Status**: 306 implemented in full, 0 partial (what is missing is listed).
|
||||
|
||||
|
|
@ -47,10 +47,10 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
|||
| Effect | Id | Params | GPU | 32 | Status |
|
||||
|---|---|---:|:-:|:-:|---|
|
||||
| Bilateral Blur | `ec.blur.bilateral` | 3 | GPU | 32 | Implemented |
|
||||
| CC Cross Blur | `ec.blur.cccross` | 3 | | 32 | Implemented |
|
||||
| CC Radial Blur | `ec.blur.ccradial` | 4 | | 32 | Implemented |
|
||||
| CC Cross Blur | `ec.blur.cccross` | 3 | GPU | 32 | Implemented |
|
||||
| CC Radial Blur | `ec.blur.ccradial` | 4 | GPU | 32 | Implemented |
|
||||
| CC Radial Fast Blur | `ec.blur.ccradialfast` | 3 | GPU | 32 | Implemented |
|
||||
| CC Vector Blur | `ec.blur.ccvector` | 4 | | 32 | Implemented |
|
||||
| CC Vector Blur | `ec.blur.ccvector` | 4 | GPU | 32 | Implemented |
|
||||
| Camera Lens Blur | `ec.blur.cameralens` | 16 | GPU | 32 | Implemented |
|
||||
| Camera-Shake Deblur | `ec.blur.camerashakedeblur` | 8 | | 32 | Implemented |
|
||||
| Channel Blur | `ec.blur.channel` | 6 | GPU | 32 | Implemented |
|
||||
|
|
@ -69,7 +69,7 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
|||
|---|---|---:|:-:|:-:|---|
|
||||
| Arithmetic | `ec.channel.arithmetic` | 5 | GPU | 32 | Implemented |
|
||||
| Blend | `ec.channel.blend` | 4 | GPU | 32 | Implemented |
|
||||
| CC Composite | `ec.channel.cccomposite` | 3 | | 32 | Implemented |
|
||||
| CC Composite | `ec.channel.cccomposite` | 3 | GPU | 32 | Implemented |
|
||||
| Calculations | `ec.channel.calculations` | 9 | GPU | 32 | Implemented |
|
||||
| Channel Combiner | `ec.channel.combiner` | 6 | GPU | 32 | Implemented |
|
||||
| Compound Arithmetic | `ec.channel.compoundarithmetic` | 6 | GPU | 32 | Implemented |
|
||||
|
|
@ -100,7 +100,7 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
|||
| Channel Mixer | `ec.color.channelmixer` | 13 | GPU | 32 | Implemented |
|
||||
| Color Balance | `ec.color.colorbalance` | 10 | GPU | 32 | Implemented |
|
||||
| Color Balance (HLS) | `ec.color.colorbalancehls` | 3 | GPU | 32 | Implemented |
|
||||
| Color Link | `ec.color.colorlink` | 6 | | 32 | Implemented |
|
||||
| Color Link | `ec.color.colorlink` | 6 | GPU | 32 | Implemented |
|
||||
| Color Stabilizer | `ec.color.colorstabilizer` | 6 | GPU | 32 | Implemented |
|
||||
| Colorama | `ec.color.colorama` | 20 | GPU | 32 | Implemented |
|
||||
| Curves | `ec.color.curves` | 0 | GPU | 32 | Implemented |
|
||||
|
|
@ -189,31 +189,31 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
|||
| Effect | Id | Params | GPU | 32 | Status |
|
||||
|---|---|---:|:-:|:-:|---|
|
||||
| 4-Color Gradient | `ec.generate.fourcolor` | 12 | GPU | 32 | Implemented |
|
||||
| Advanced Lightning | `ec.generate.advancedlightning` | 24 | | 32 | Implemented |
|
||||
| Audio Spectrum | `ec.generate.audiospectrum` | 23 | | 32 | Implemented |
|
||||
| Audio Waveform | `ec.generate.audiowaveform` | 17 | | 32 | Implemented |
|
||||
| Beam | `ec.generate.beam` | 11 | | 32 | Implemented |
|
||||
| CC Glue Gun | `ec.generate.ccgluegun` | 7 | | 32 | Implemented |
|
||||
| Advanced Lightning | `ec.generate.advancedlightning` | 24 | GPU | 32 | Implemented |
|
||||
| Audio Spectrum | `ec.generate.audiospectrum` | 23 | GPU | 32 | Implemented |
|
||||
| Audio Waveform | `ec.generate.audiowaveform` | 17 | GPU | 32 | Implemented |
|
||||
| Beam | `ec.generate.beam` | 11 | GPU | 32 | Implemented |
|
||||
| CC Glue Gun | `ec.generate.ccgluegun` | 7 | GPU | 32 | Implemented |
|
||||
| CC Light Burst 2.5 | `ec.generate.cclightburst` | 6 | GPU | 32 | Implemented |
|
||||
| CC Light Rays | `ec.generate.cclightrays` | 8 | GPU | 32 | Implemented |
|
||||
| CC Light Sweep | `ec.generate.cclightsweep` | 9 | GPU | 32 | Implemented |
|
||||
| CC Threads | `ec.generate.ccthreads` | 8 | | 32 | Implemented |
|
||||
| CC Threads | `ec.generate.ccthreads` | 8 | GPU | 32 | Implemented |
|
||||
| Cell Pattern | `ec.generate.cellpattern` | 14 | GPU | 32 | Implemented |
|
||||
| Checkerboard | `ec.generate.checkerboard` | 10 | GPU | 32 | Implemented |
|
||||
| Circle | `ec.generate.circle` | 11 | GPU | 32 | Implemented |
|
||||
| Ellipse | `ec.generate.ellipse` | 8 | GPU | 32 | Implemented |
|
||||
| Eyedropper Fill | `ec.generate.eyedropperfill` | 5 | | 32 | Implemented |
|
||||
| Eyedropper Fill | `ec.generate.eyedropperfill` | 5 | GPU | 32 | Implemented |
|
||||
| Fill | `ec.generate.fill` | 7 | GPU | 32 | Implemented |
|
||||
| Fractal | `ec.generate.fractal` | 21 | | 32 | Implemented |
|
||||
| Gradient Ramp | `ec.generate.gradientramp` | 7 | GPU | 32 | Implemented |
|
||||
| Grid | `ec.generate.grid` | 12 | GPU | 32 | Implemented |
|
||||
| Lens Flare | `ec.generate.lensflare` | 4 | | 32 | Implemented |
|
||||
| Paint Bucket | `ec.generate.paintbucket` | 11 | | 32 | Implemented |
|
||||
| Radio Waves | `ec.generate.radiowaves` | 31 | | 32 | Implemented |
|
||||
| Scribble | `ec.generate.scribble` | 25 | | 32 | Implemented |
|
||||
| Stroke | `ec.generate.stroke` | 11 | | 32 | Implemented |
|
||||
| Vegas | `ec.generate.vegas` | 26 | | 32 | Implemented |
|
||||
| Write-on | `ec.generate.writeon` | 10 | | 32 | Implemented |
|
||||
| Lens Flare | `ec.generate.lensflare` | 4 | GPU | 32 | Implemented |
|
||||
| Paint Bucket | `ec.generate.paintbucket` | 11 | GPU | 32 | Implemented |
|
||||
| Radio Waves | `ec.generate.radiowaves` | 31 | GPU | 32 | Implemented |
|
||||
| Scribble | `ec.generate.scribble` | 25 | GPU | 32 | Implemented |
|
||||
| Stroke | `ec.generate.stroke` | 11 | GPU | 32 | Implemented |
|
||||
| Vegas | `ec.generate.vegas` | 26 | GPU | 32 | Implemented |
|
||||
| Write-on | `ec.generate.writeon` | 10 | GPU | 32 | Implemented |
|
||||
|
||||
## Immersive Video
|
||||
|
||||
|
|
@ -237,12 +237,12 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
|||
| Effect | Id | Params | GPU | 32 | Status |
|
||||
|---|---|---:|:-:|:-:|---|
|
||||
| Advanced Spill Suppressor | `ec.key.advancedspill` | 8 | GPU | 32 | Implemented |
|
||||
| CC Simple Wire Removal | `ec.key.ccsimplewireremoval` | 7 | | 32 | Implemented |
|
||||
| CC Simple Wire Removal | `ec.key.ccsimplewireremoval` | 7 | GPU | 32 | Implemented |
|
||||
| Color Difference Key | `ec.key.colordifference` | 16 | GPU | 32 | Implemented |
|
||||
| Color Range | `ec.key.colorrange` | 8 | GPU | 32 | Implemented |
|
||||
| Difference Matte | `ec.key.differencematte` | 6 | GPU | 32 | Implemented |
|
||||
| Extract | `ec.key.extract` | 6 | GPU | 32 | Implemented |
|
||||
| Inner/Outer Key | `ec.key.innerouter` | 88 | | 32 | Implemented |
|
||||
| Inner/Outer Key | `ec.key.innerouter` | 88 | GPU | 32 | Implemented |
|
||||
| Key Cleaner | `ec.key.keycleaner` | 4 | GPU | 32 | Implemented |
|
||||
| Key Light | `ec.keying.keylight` | 56 | GPU | 32 | Implemented |
|
||||
| Linear Color Key | `ec.key.linearcolor` | 6 | GPU | 32 | Implemented |
|
||||
|
|
@ -282,14 +282,14 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
|||
|
||||
| Effect | Id | Params | GPU | 32 | Status |
|
||||
|---|---|---:|:-:|:-:|---|
|
||||
| Basic 3D | `ec.obsolete.basic3d` | 5 | | 32 | Implemented |
|
||||
| Basic Text | `ec.obsolete.basictext` | 11 | | 32 | Implemented |
|
||||
| Basic 3D | `ec.obsolete.basic3d` | 5 | GPU | 32 | Implemented |
|
||||
| Basic Text | `ec.obsolete.basictext` | 11 | GPU | 32 | Implemented |
|
||||
| Color Key | `ec.key.colorkey` | 4 | GPU | 32 | Implemented |
|
||||
| Gaussian Blur (Legacy) | `ec.obsolete.gaussianlegacy` | 2 | GPU | 32 | Implemented |
|
||||
| Lightning | `ec.obsolete.lightning` | 25 | | 32 | Implemented |
|
||||
| Lightning | `ec.obsolete.lightning` | 25 | GPU | 32 | Implemented |
|
||||
| Luma Key | `ec.key.luma` | 5 | GPU | 32 | Implemented |
|
||||
| Path Text | `ec.obsolete.pathtext` | 32 | | 32 | Implemented |
|
||||
| Reduce Interlace Flicker | `ec.blur.reduceflicker` | 1 | | 32 | Implemented |
|
||||
| Path Text | `ec.obsolete.pathtext` | 32 | GPU | 32 | Implemented |
|
||||
| Reduce Interlace Flicker | `ec.blur.reduceflicker` | 1 | GPU | 32 | Implemented |
|
||||
| Spill Suppressor | `ec.key.spill` | 3 | GPU | 32 | Implemented |
|
||||
|
||||
## Paint
|
||||
|
|
@ -341,13 +341,13 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
|||
| Effect | Id | Params | GPU | 32 | Status |
|
||||
|---|---|---:|:-:|:-:|---|
|
||||
| Brush Strokes | `ec.stylize.brushstrokes` | 7 | GPU | 32 | Implemented |
|
||||
| CC Block Load | `ec.stylize.ccblockload` | 5 | | 32 | Implemented |
|
||||
| CC Burn Film | `ec.stylize.ccburnfilm` | 3 | | 32 | Implemented |
|
||||
| CC Glass | `ec.stylize.ccglass` | 17 | | 32 | Implemented |
|
||||
| CC HexTile | `ec.stylize.cchextile` | 5 | | 32 | Implemented |
|
||||
| CC Block Load | `ec.stylize.ccblockload` | 5 | GPU | 32 | Implemented |
|
||||
| CC Burn Film | `ec.stylize.ccburnfilm` | 3 | GPU | 32 | Implemented |
|
||||
| CC Glass | `ec.stylize.ccglass` | 17 | GPU | 32 | Implemented |
|
||||
| CC HexTile | `ec.stylize.cchextile` | 5 | GPU | 32 | Implemented |
|
||||
| CC Kaleida | `ec.stylize.cckaleida` | 4 | GPU | 32 | Implemented |
|
||||
| CC Mr. Smoothie | `ec.stylize.ccmrsmoothie` | 7 | | 32 | Implemented |
|
||||
| CC Plastic | `ec.stylize.ccplastic` | 18 | | 32 | Implemented |
|
||||
| CC Mr. Smoothie | `ec.stylize.ccmrsmoothie` | 7 | GPU | 32 | Implemented |
|
||||
| CC Plastic | `ec.stylize.ccplastic` | 18 | GPU | 32 | Implemented |
|
||||
| CC RepeTile | `ec.stylize.ccrepetile` | 5 | GPU | 32 | Implemented |
|
||||
| CC Threshold | `ec.stylize.ccthreshold` | 4 | GPU | 32 | Implemented |
|
||||
| CC Threshold RGB | `ec.stylize.ccthresholdrgb` | 7 | GPU | 32 | Implemented |
|
||||
|
|
@ -413,12 +413,12 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
|||
| Effect | Id | Params | GPU | 32 | Status |
|
||||
|---|---|---:|:-:|:-:|---|
|
||||
| Apply Color LUT | `ec.utility.applylut` | 0 | GPU | 32 | Implemented |
|
||||
| CC Overbrights | `ec.utility.ccoverbrights` | 2 | | 32 | Implemented |
|
||||
| Cineon Converter | `ec.utility.cineon` | 7 | | 32 | Implemented |
|
||||
| CC Overbrights | `ec.utility.ccoverbrights` | 2 | GPU | 32 | Implemented |
|
||||
| Cineon Converter | `ec.utility.cineon` | 7 | GPU | 32 | Implemented |
|
||||
| Color Profile Converter | `ec.utility.colorprofileconverter` | 7 | GPU | 32 | Implemented |
|
||||
| Face Measurements | `ec.utility.facemeasurements` | 14 | | 32 | Implemented |
|
||||
| Face Track Points | `ec.utility.facetrackpoints` | 26 | | 32 | Implemented |
|
||||
| Grow Bounds | `ec.utility.growbounds` | 1 | | 32 | Implemented |
|
||||
| HDR Compander | `ec.utility.hdrcompander` | 3 | | 32 | Implemented |
|
||||
| HDR Highlight Compression | `ec.utility.hdrcompression` | 1 | | 32 | Implemented |
|
||||
| Grow Bounds | `ec.utility.growbounds` | 1 | GPU | 32 | Implemented |
|
||||
| HDR Compander | `ec.utility.hdrcompander` | 3 | GPU | 32 | Implemented |
|
||||
| HDR Highlight Compression | `ec.utility.hdrcompression` | 1 | GPU | 32 | Implemented |
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ Partial features:
|
|||
|
||||
| id | Tier | Done | What is missing |
|
||||
|---|---|---|---|
|
||||
| EFF-5 GPU effects | P1 | 0.93 | GPU kernels for 50 visual effects (CC blur/glass family, generators such as Lightning, Beam, Lens Flare, Stroke, Write-on, Audio Spectrum, and analysis/tool effects). M13.29 moved the particle effects (CC Particle World / Systems II, Particle Playground, CC Ball Action, CC Pixel Polly, CC Scatterize) and Curl Noise onto the GPU and removed the Shatter wireframe, Foam extras, Timewarp matte, strong Warp Fisheye / Twist and custom `.ocio` fallbacks. The 13 audio filters stay on the CPU by design |
|
||||
| EFF-5 GPU effects | P1 | 0.98 | Fractal (its escape iteration needs f64: WGSL has none and Metal's fast math defeats double-f32 emulation) and the analysis / tool effects (Camera-Shake Deblur, Detail-preserving Upscale, Puppet, Rolling Shutter Repair, Warp Stabilizer, Mocha Shape, Roto Brush, Match Grain, Paint, Camera Tracker, Face Tracker) render on the CPU. M13.28 moved the remaining pixel effects (CC blur / glass family, Color Link, Cineon, HDR, Inner/Outer Key, Basic 3D…) and the geometry and audio generators (Lightning, Advanced Lightning, Beam, Lens Flare, Radio Waves, Vegas, Stroke, Scribble, Write-on, Paint Bucket, Eyedropper Fill, CC Glue Gun, CC Threads, Audio Spectrum / Waveform, Basic / Path Text) onto the GPU; M13.29 the particle effects (CC Particle World / Systems II, Particle Playground, CC Ball Action, CC Pixel Polly, CC Scatterize) and Curl Noise, and removed the Shatter wireframe, Foam extras, Timewarp matte, strong Warp Fisheye / Twist and custom `.ocio` fallbacks. The 13 audio filters stay on the CPU by design |
|
||||
| MSK-4 Roto Brush | P2 | 0.85 | Segmentation is classical (graph cut + optical flow), not a learned model |
|
||||
| TRK-3 Face tracking | P2 | 0.9 | Classical fitter: weak on profile and occluded faces; Rolling Shutter Ripple is approximated |
|
||||
|
||||
|
|
@ -153,7 +153,7 @@ the disk cache in the formats wave; the 3D Camera Tracker in M12.6.)
|
|||
| Text | ≈ 96% | 0.3 | no extruded strokes (M13.12: the Variable Font Axes animator re-spaces the text — advances follow the animated axes; M13.6: variable font axes in the character style — `layer.setText variations`, the Character panel's Variable Font Axes fields — shape with HVAR / gvar advances and draw at that design-space position; OpenType features — stylistic sets, discretionary ligatures, contextual / stylistic alternates, swash, titling, ordinals, fractions, figure styles, true small caps / all small caps and superior / inferior glyphs with faux fallback — per character with the Character panel's OpenType popup and `text.fontFeatures` landed in M13.2; vertical Roman / Tate-Chu-Yoko, forced LTR paragraphs, caret on animated and path text, Variable Font Axes and Lottie style runs landed in M13.5; extruded, bevelled text in M7.6; per-character styles, paragraph settings, on-canvas editing and the `sourceText` style API in M9.9–M9.10) |
|
||||
| Web | 97% | 0.2 | GPU effects in Render Queue / analysis workers (job workers render on their CPU), layer buffers in the browser's disk cache (frames only), no shared-memory threads inside one engine instance (the decided design is one engine instance per worker; a threaded build needs nightly `build-std`) (browser storage, Web Audio, Web Worker renders/analyses, WebGPU viewer and offline install landed in M15.2; viewer frames in frame workers fed by project diffs, Roto Brush propagation in a worker, non-blocking `wait: true` jobs and a browser Media Browser (File System Access folders, browser storage) in M13.10; WEB-1 in M13.24: GPU effects and the GPU compositor in the frame workers on their own WebGPU devices with deferred readbacks (frames render in passes; Backend Auto per comp), the disk cache in the Origin Private File System (written by the workers with sync access handles, LRU under the settings' limit, served after a reload), the storage manager (Settings ▸ Disk ▸ Browser Storage, `storage.info` / `storage.persist` / `storage.clear`), and a WGSL constant Chrome rejected (it disabled every GPU kernel in Chrome) fixed) |
|
||||
| 3D | 88% | 5.5 | multi-view layouts, the Extended Viewer for Advanced 3D comps (Classic 3D Extended Viewer landed in M13.5 UI completion); collapsed precomps of another size seen through the parent's camera render (fixed in M13.2); stereo rigs, orbit nulls, lights controlled by the camera, cameras/lights from glTF models, environment backgrounds, Advanced 3D motion blur, blend modes and track mattes landed in M7.7; Classic 3D iris-shaped bokeh with highlights, progressive depth of field on tilted layers and the focus-link commands landed in M13.5; Advanced 3D depth of field with the iris and highlight options, collapsed precomps as real Advanced 3D geometry and extruded text/shape strokes landed in M13.8; Advanced 3D (glTF/OBJ models, primitives, extruded text and shapes, PBR, image-based light, shadow maps, GPU rasteriser) in M7.4–M7.6; Advanced 3D end to end on the GPU (motion blur, iris depth of field, compositing) in M13.11 |
|
||||
| Effects | ≈ 85% | 3.0 | GPU versions of the remaining effects (243 run on the GPU since M13.29, EFF-5: M13.29 added the particle effects (CC Particle World / Systems II, Particle Playground, CC Ball Action, CC Pixel Polly, CC Scatterize, Curl Noise) and removed the fallbacks of Shatter's wireframe views, Foam's extras, Timewarp's Matte Layer, strong Warp Fisheye / Twist bends and custom `.ocio` configs; M13.23 added the 3D Channel and Immersive Video families, Apply Color LUT, the OCIO effects, Color Profile Converter and the simulations' render passes (Card Wipe included); M13.22 added the CC light family, the CC transitions, Block Dissolve, Radial Shadow, CC Bender / Blobbylize / Cylinder / Sphere / Spotlight / Environment, 3D Glasses, Numbers, Timecode and the time effects joined the 166 of M13.13; audio-driven effects still render on the CPU) and the missing controls listed as partial in [effects.md](effects.md) (every After Effects effect exists since M9.11, M12.5 and M12.6; parameter names, order, twirl-downs, popups, units and defaults were aligned in M9.12) |
|
||||
| Effects | ≈ 85% | 3.0 | GPU versions of the remaining effects (280 run on the GPU since M13.29, EFF-5: M13.28 added the remaining pixel effects and the geometry and audio generators (Lightning, Advanced Lightning, Beam, Lens Flare, Radio Waves, Vegas, Stroke, Scribble, Write-on, Paint Bucket, Eyedropper Fill, CC Glue Gun, CC Threads, Audio Spectrum / Waveform, Basic / Path Text); M13.29 added the particle effects (CC Particle World / Systems II, Particle Playground, CC Ball Action, CC Pixel Polly, CC Scatterize, Curl Noise) and removed the fallbacks of Shatter's wireframe views, Foam's extras, Timewarp's Matte Layer, strong Warp Fisheye / Twist bends and custom `.ocio` configs; M13.23 added the 3D Channel and Immersive Video families, Apply Color LUT, the OCIO effects, Color Profile Converter and the simulations' render passes (Card Wipe included); M13.22 added the CC light family, the CC transitions, Block Dissolve, Radial Shadow, CC Bender / Blobbylize / Cylinder / Sphere / Spotlight / Environment, 3D Glasses, Numbers, Timecode and the time effects joined the 166 of M13.13; Fractal and the analysis / tool effects still render on the CPU) and the missing controls listed as partial in [effects.md](effects.md) (every After Effects effect exists since M9.11, M12.5 and M12.6; parameter names, order, twirl-downs, popups, units and defaults were aligned in M9.12) |
|
||||
| Interface | 75% | 5.0 | more Learn tutorials and pixel-level fidelity of dialogs (the Home ▸ Templates gallery (eight original built-in templates, user templates from File ▸ Save as Template…) and View ▸ Simulate Output ▸ My Custom RGB… landed in M13.25; Timeline layer reordering by drag, a non-snapping viewer pan, a working rename field and twirl arrows, and the full set of property reveal shortcuts — double presses, Alt+Shift keyframes, Ctrl+` — landed in M13.17–M13.20; the Home ▸ Learn tab with interactive tutorials and a UI fidelity pass landed in M13.5 UI completion; visual editors for Lumetri RGB / hue-saturation curves, Colorama's output cycle wheel, Glow's colour map and Reshape's correspondence points (viewer handles), a scrolling Preview panel and AE-style Composition / Timeline tabs (close, label swatch, viewer lock) landed in M13.10; Timeline outline and Project panel columns scroll horizontally, the Layer Style dialog, ROI resize handles, Pan Behind snapping and 3D Reference Axes landed in M13.5; native macOS menu bar, Timeline columns/search/reveal-add, Home screen with recent projects and all AE workspaces landed; viewer rulers/snapping/channels/snapshots landed in M0.13) |
|
||||
| Project | ≈ 68% | 5.5 | auto-save, folder moves, OCIO displays beyond the built-in tone map (Color Engine with OCIO/ACES working spaces, HDR compand/tone mapping, Rec. 2100 PQ/HLG output, Feet + Frames, display colour management, Simulate Output and the locked viewer landed in M7.7; proxies and Interpret Footage fields / pixel aspect / alpha guess landed: PRJ-8, PRJ-3) |
|
||||
| Masks & roto | 74% | 5.0 | Roto Brush's learned (3.0) segmentation model (variable-width mask feather points with the Mask Feather tool landed in M13.5; mask tracking and Mask Interpolation landed in M6.6; Roto Brush & Refine Edge with graph-cut segmentation, flow propagation, edge matting, decontamination and Freeze in M6.7) |
|
||||
|
|
@ -457,9 +457,41 @@ composited at 8 and 32 bpc:
|
|||
default to the layer). Shatter's wireframe views and Foam's User Defined texture,
|
||||
Environment Map and flow-map preview render on the CPU.
|
||||
|
||||
## Update: M13.28 GPU effects, part C (EFF-5)
|
||||
|
||||
37 more effects run on the GPU, each checked against the CPU oracle (≤ 1/255 at 8 bpc, ≤ 1e-3 at
|
||||
32 bpc) directly on a buffer with masks, another layer and an audio track (full and half
|
||||
resolution, adjustment) and composited at 8 and 32 bpc:
|
||||
|
||||
- **Pixel ports** (`gpu::fx_pixel2`): CC Cross Blur, CC Radial Blur, CC Vector Blur, Reduce
|
||||
Interlace Flicker, CC Composite, Color Link, Cineon Converter, HDR Compander, HDR Highlight
|
||||
Compression, Grow Bounds, CC Overbrights, CC Block Load, CC Burn Film, CC Glass, CC HexTile,
|
||||
CC Mr. Smoothie, CC Plastic, Inner/Outer Key, CC Simple Wire Removal and Basic 3D.
|
||||
Inner/Outer Key's trimap and Cleanup strokes and CC Glass / Plastic's light are built next to
|
||||
the CPU effect and shared; Color Link's sample statistics run on the CPU over the read-back
|
||||
layer (none when a Source Layer is chosen). Plane blurs (`util::gauss_plane`) sum each box
|
||||
window directly, as the CPU's f64 running sums keep the small values that normalised blurs
|
||||
divide. CC Plastic now reads its Bump Layer (it read a nonexistent parameter id).
|
||||
- **Generators** (`gpu::fx_gen2`): Lightning, Advanced Lightning, Beam, Lens Flare, Radio Waves,
|
||||
Vegas, Stroke, Scribble, Write-on, Paint Bucket, Eyedropper Fill, CC Glue Gun, CC Threads,
|
||||
Audio Spectrum, Audio Waveform, Basic Text and Path Text. The geometry stays on the CPU and is
|
||||
shared with the CPU effect as a per-frame plan (`effects::bolt_plan`, `lightning_segments`,
|
||||
`paint_plan`, `marks_plan` with the FFT / peak analysis, `text_passes`, `radio_plan`, Paint
|
||||
Bucket's flood-filled region, Eyedropper Fill's sample); segments and convex polygons are
|
||||
binned into 16 × 16 tiles and rasterised on the GPU with the CPU's coverage functions, glows
|
||||
and compositing included. The CPU effects were refactored to draw from the same plans with
|
||||
bit-identical output (`crates/effects/tests/gen_golden.rs`: 288 frame hashes pinned before
|
||||
the refactor). Exception: CC Threads decides band membership and fibre shade per pixel, and at
|
||||
round parameter values (the defaults) pixel centres sit exactly on band edges, where f32 and
|
||||
the CPU's f64 can fall on either side: up to 5 % of pixels allowed (measured ≤ 3.6 % at half
|
||||
resolution with the defaults, 0 at generic values).
|
||||
- **Fractal** stays on the CPU: in f32, orbits near the set escape at other iterations on 0.3–1.5 %
|
||||
of the pixels even at low magnification, WGSL has no f64 and Metal's fast math defeats
|
||||
double-f32 emulation.
|
||||
|
||||
## Update: M13.29 GPU effects, part D (EFF-5)
|
||||
|
||||
7 more effects run on the GPU (243 in all), and five CPU fallbacks are gone. Each case is
|
||||
7 more effects run on the GPU (280 in all, with M13.28's 37), and five CPU fallbacks are gone. Each case is
|
||||
checked against the CPU oracle (≤ 1/255 at 8 bpc, ≤ 1e-3 at 32 bpc) directly on a buffer (full
|
||||
and half resolution) and composited at 8 and 32 bpc; the CPU refactors behind the shared plans
|
||||
are pinned bit for bit by golden hashes (`crates/effects/tests/particle_golden.rs`,
|
||||
|
|
|
|||
Loading…
Reference in New Issue