mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-24 10:09:32 -06:00
M13.28: GPU generators (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/Waveform, Basic Text, Path Text
The geometry stays on the CPU as per-frame plans shared with the CPU effects (bolts, mask strokes, scribbles, contour segments, audio marks, glyph strokes, waves, flood-fill region, eyedropper sample); segments and convex polygons are tile-binned and rasterised on the GPU with the CPU's coverage functions. CPU output unchanged (gen_golden). CC Threads' exact ties at round parameter values are a documented exception; Fractal stays on the CPU (needs f64). Regenerated docs/effects.md, docs/parity.md EFF-5. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
5df9cde182
commit
c4315818da
@@ -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"));
|
||||
|
||||
+213
-68
@@ -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
|
||||
|
||||
@@ -89,7 +89,12 @@ 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};
|
||||
@@ -1040,6 +1045,24 @@ pub const GPU_EFFECTS: &[&str] = &[
|
||||
"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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -325,6 +325,7 @@ impl GpuContext {
|
||||
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 +395,7 @@ impl GpuContext {
|
||||
.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,7 @@ fn apply(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
_ 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)
|
||||
}
|
||||
+10
-6
@@ -22,14 +22,15 @@
|
||||
//! 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_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`).
|
||||
//! (`fx_color`, `fx_depth`, `fx_distort`, `fx_extra`, `fx_gen2`, `fx_generate`, `fx_key`,
|
||||
//! `fx_light`, `fx_lut`, `fx_noise`, `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_lut`), simulations keep their state on the CPU and rasterise its per-frame plan here
|
||||
//! (`fx_sim`), 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,6 +60,7 @@ mod fx_color;
|
||||
mod fx_depth;
|
||||
mod fx_distort;
|
||||
mod fx_extra;
|
||||
mod fx_gen2;
|
||||
mod fx_generate;
|
||||
mod fx_key;
|
||||
mod fx_light;
|
||||
@@ -323,6 +325,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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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()),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -120,6 +120,11 @@ pub(crate) fn env_case(id: &str, vals: &[(&str, Value)]) {
|
||||
|
||||
/// [`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);
|
||||
@@ -135,8 +140,8 @@ pub(crate) fn env_case_allow(id: &str, vals: &[(&str, Value)], allow: f64) {
|
||||
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(), Tick::ZERO), allow);
|
||||
check(&format!("{id} {vals:?} {depth:?} half"), compare_at(&s, RenderOpts { scale: 0.5, ..opts() }, Tick::ZERO), allow);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+18
-18
@@ -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**: 256 effects also run on the GPU compositor with identical results.
|
||||
- **GPU**: 273 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).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -283,12 +283,12 @@ 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 | GPU | 32 | Implemented |
|
||||
| Basic Text | `ec.obsolete.basictext` | 11 | | 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 |
|
||||
| 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 |
|
||||
|
||||
|
||||
+34
-2
@@ -19,7 +19,7 @@ Partial features:
|
||||
|
||||
| id | Tier | Done | What is missing |
|
||||
|---|---|---|---|
|
||||
| EFF-5 GPU effects | P1 | 0.97 | GPU kernels for the audio-driven effects and the stepped particle systems' sprite pass (the CC light / transition families, Numbers, Timecode and Time effects landed in M13.22; 3D Channel, the VR family, OCIO / LUT and the simulations' render passes in M13.23); custom `.ocio` configs, Shatter's wireframe views and Foam's texture / environment / flow-map extras render on the CPU |
|
||||
| EFF-5 GPU effects | P1 | 0.98 | GPU kernels for the stepped particle systems' sprite pass and Fractal (its escape iteration needs f64: WGSL has none and Metal's fast math defeats double-f32 emulation); custom `.ocio` configs, Shatter's wireframe views and Foam's texture / environment / flow-map extras render on the CPU (the CC light / transition families, Numbers, Timecode and Time effects landed in M13.22; 3D Channel, the VR family, OCIO / LUT and the simulations' render passes in M13.23; the remaining pixel effects, the geometry generators and the audio-driven effects in M13.28) |
|
||||
| WEB-1 Web app | P0 | 0.97 | GPU effects in Render Queue / analysis job workers (they render on their CPU); layer buffers in the browser's disk cache (frames only); threads inside one engine instance (the decided design is one engine instance per worker; a threaded build needs nightly `build-std`). M13.24 added GPU effects and the GPU compositor in the frame workers (their own WebGPU devices, 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.*`) and fixed a WGSL constant Chrome rejected (it disabled every GPU kernel in Chrome) |
|
||||
| UI-7 Home screen | P1 | 0.95 | A "start from a template" gallery |
|
||||
| MSK-4 Roto Brush | P2 | 0.85 | Segmentation is classical (graph cut + optical flow), not a learned model |
|
||||
@@ -156,7 +156,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 (236 run on the GPU since M13.23, EFF-5: 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 and the stepped particle systems 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 (273 run on the GPU since M13.28, EFF-5: 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.28 the remaining pixel effects (CC Cross / Radial / Vector Blur, Color Link, Cineon, HDR, CC Block Load / Burn Film / Glass / HexTile / Mr. Smoothie / Plastic, Inner/Outer Key, Basic 3D…) and the 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 Text, Path Text); 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; the stepped particle systems and Fractal 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) |
|
||||
@@ -460,6 +460,38 @@ 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.
|
||||
|
||||
## Highest-value gaps, in order
|
||||
|
||||
1. ~~On-canvas text editing and per-character styles~~ (landed: M9.9–M9.10).
|
||||
|
||||
Reference in New Issue
Block a user