mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-27 19:47:12 -06:00
Merge branch 'main' into worktree-agent-abff9a10f06a98f2c
# Conflicts: # crates/gpu/src/tests_deferred.rs
This commit is contained in:
@@ -325,6 +325,8 @@ impl GpuContext {
|
||||
include_str!("shaders/fx_transition.wgsl"),
|
||||
include_str!("shaders/fx_text.wgsl"),
|
||||
include_str!("shaders/fx_time.wgsl"),
|
||||
include_str!("shaders/fx_pixel2.wgsl"),
|
||||
include_str!("shaders/fx_gen2.wgsl"),
|
||||
include_str!("shaders/sky.wgsl"),
|
||||
]
|
||||
.concat();
|
||||
@@ -394,6 +396,8 @@ impl GpuContext {
|
||||
.chain(crate::fx_transition::KERNELS)
|
||||
.chain(crate::fx_text::KERNELS)
|
||||
.chain(crate::fx_time::KERNELS)
|
||||
.chain(crate::fx_pixel2::KERNELS)
|
||||
.chain(crate::fx_gen2::KERNELS)
|
||||
.chain(crate::adv3d::SKY_KERNELS)
|
||||
.map(|e| (e, &layout))
|
||||
.chain(EXT_ENTRIES.iter().map(|e| (e, &layout_ext)))
|
||||
|
||||
@@ -176,6 +176,8 @@ fn apply(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
_ if crate::fx_transition::IDS.contains(&id) => crate::fx_transition::apply(e, id, ctx, b),
|
||||
_ if crate::fx_text::IDS.contains(&id) => crate::fx_text::apply(e, id, ctx, b),
|
||||
_ if crate::fx_time::IDS.contains(&id) => crate::fx_time::apply(e, id, ctx, b),
|
||||
_ if crate::fx_pixel2::IDS.contains(&id) => crate::fx_pixel2::apply(e, id, ctx, b),
|
||||
_ if crate::fx_gen2::IDS.contains(&id) => crate::fx_gen2::apply(e, id, ctx, b),
|
||||
_ => pointwise(e, id, ctx, b),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,561 @@
|
||||
//! GPU effects, generators of part C (kernels in `shaders/fx_gen2.wgsl`, every entry point
|
||||
//! prefixed `fg2_`): Lightning, Advanced Lightning, Beam, Fractal, Lens Flare, Radio Waves,
|
||||
//! Vegas, Stroke, Scribble, Write-on, Paint Bucket, Eyedropper Fill, CC Glue Gun, CC Threads,
|
||||
//! Audio Spectrum, Audio Waveform, Basic Text and Path Text.
|
||||
//!
|
||||
//! Geometry stays on the CPU and is shared with the CPU effect as a per-frame plan: the bolts
|
||||
//! (`effects::bolt_plan`, `effects::lightning_segments`), mask strokes, scribbles and contour
|
||||
//! segments (`effects::paint_plan`), audio marks (`effects::marks_plan`: the FFT and peak
|
||||
//! analysis), glyph strokes (`effects::text_passes`), live waves and their contour
|
||||
//! (`effects::radio_plan`), Paint Bucket's flood-filled region and Eyedropper Fill's sampled
|
||||
//! colour (both over the read-back layer). The plans' segments and polygons are binned into
|
||||
//! 16 × 16 tiles on the CPU and rasterised here with the CPU's coverage functions
|
||||
//! (`raster_segs`' smooth round caps, `raster_convex`'s 4 × 4 samples); the colouring, glows
|
||||
//! (util::gauss_plane) and compositing run here too. Beam, Lens Flare, CC Glue Gun and CC
|
||||
//! Threads are closed-form kernels.
|
||||
//!
|
||||
//! Fractal stays on the CPU: its escape iteration needs f64 (WGSL has none, and Metal's fast
|
||||
//! math defeats double-f32 emulation); in f32, orbits near the set escape at other iterations
|
||||
//! on 0.3–1.5 % of the pixels even at low magnification.
|
||||
|
||||
use effectcraft_effects::{Buf, Coverage, EffectCtx};
|
||||
use effectcraft_raster::Image;
|
||||
|
||||
use crate::context::{Enc, GpuImage, Params};
|
||||
use crate::effects::GBuf;
|
||||
use crate::fx_pixel2::gauss_plane;
|
||||
|
||||
/// Compute entry points in `fx_gen2.wgsl`.
|
||||
pub(crate) const KERNELS: &[&str] = &[
|
||||
"fg2_segs",
|
||||
"fg2_convex",
|
||||
"fg2_paint",
|
||||
"fg2_marks",
|
||||
"fg2_pair",
|
||||
"fg2_bolt",
|
||||
"fg2_adv_bolt",
|
||||
"fg2_text",
|
||||
"fg2_radio",
|
||||
"fg2_beam",
|
||||
"fg2_flare",
|
||||
"fg2_glue",
|
||||
"fg2_threads",
|
||||
"fg2_bucket",
|
||||
"fg2_bucket_cov",
|
||||
"fg2_eyedropper",
|
||||
];
|
||||
|
||||
/// Effect ids implemented here.
|
||||
pub(crate) const IDS: &[&str] = &[
|
||||
"ec.obsolete.lightning",
|
||||
"ec.generate.advancedlightning",
|
||||
"ec.generate.beam",
|
||||
"ec.generate.lensflare",
|
||||
"ec.generate.radiowaves",
|
||||
"ec.generate.vegas",
|
||||
"ec.generate.stroke",
|
||||
"ec.generate.scribble",
|
||||
"ec.generate.writeon",
|
||||
"ec.generate.paintbucket",
|
||||
"ec.generate.eyedropperfill",
|
||||
"ec.generate.ccgluegun",
|
||||
"ec.generate.ccthreads",
|
||||
"ec.generate.audiospectrum",
|
||||
"ec.generate.audiowaveform",
|
||||
"ec.obsolete.basictext",
|
||||
"ec.obsolete.pathtext",
|
||||
];
|
||||
|
||||
/// Largest item table (floats) the raster kernels take; bigger plans render on the CPU.
|
||||
const MAX_FLOATS: usize = 1 << 24;
|
||||
const TILE: i32 = 16;
|
||||
/// "No minimum radius" for a segment variant.
|
||||
const NO_MIN: f32 = -3.0e38;
|
||||
|
||||
/// Run effect `id` (one of [`IDS`]); `None` = this parameter combination runs on the CPU.
|
||||
pub(crate) fn apply(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
match id {
|
||||
"ec.obsolete.lightning" => lightning(e, ctx, b),
|
||||
"ec.generate.advancedlightning" => advanced_lightning(e, ctx, b),
|
||||
"ec.generate.beam" => beam(e, ctx, b),
|
||||
"ec.generate.lensflare" => lens_flare(e, ctx, b),
|
||||
"ec.generate.radiowaves" => radio_waves(e, ctx, b),
|
||||
"ec.generate.vegas" | "ec.generate.stroke" | "ec.generate.scribble" | "ec.generate.writeon" => paint(e, id, ctx, b),
|
||||
"ec.generate.paintbucket" => paint_bucket(e, ctx, b),
|
||||
"ec.generate.eyedropperfill" => eyedropper(e, ctx, b),
|
||||
"ec.generate.ccgluegun" => glue_gun(e, ctx, b),
|
||||
"ec.generate.ccthreads" => threads(e, ctx, b),
|
||||
"ec.generate.audiospectrum" | "ec.generate.audiowaveform" => marks(e, id, ctx, b),
|
||||
"ec.obsolete.basictext" | "ec.obsolete.pathtext" => text(e, id, ctx, b),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// A same-size per-pixel kernel over the buffer.
|
||||
fn run(e: &mut Enc, entry: &str, p: &Params, mut b: GBuf, aux: Option<&GpuImage>, data: Option<&wgpu::Buffer>) -> Option<GBuf> {
|
||||
let out = e.scratch(b.img.width, b.img.height);
|
||||
e.pixels(entry, p, &b.img, aux, &out, data);
|
||||
b.img = out;
|
||||
Some(b)
|
||||
}
|
||||
|
||||
/// The buffer on the CPU for building a plan: its pixels when `pixels` (read back), else only
|
||||
/// its geometry.
|
||||
fn cpu_buf(e: &mut Enc, b: &GBuf, pixels: bool) -> Option<Buf> {
|
||||
let img = if pixels { e.download(&b.img)? } else { Image { width: b.img.width, height: b.img.height, data: vec![] } };
|
||||
Some(Buf { img, offset: b.offset, scale: b.scale })
|
||||
}
|
||||
|
||||
fn rgb(c: [f32; 4]) -> [f32; 4] {
|
||||
[c[0], c[1], c[2], 0.0]
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- tiled item raster
|
||||
|
||||
/// Items with their integer pixel bounds, binned into 16 × 16 tiles.
|
||||
struct Items {
|
||||
stride: usize,
|
||||
recs: Vec<f32>,
|
||||
w: i32,
|
||||
h: i32,
|
||||
tiles: Vec<Vec<u32>>,
|
||||
tx: i32,
|
||||
}
|
||||
|
||||
impl Items {
|
||||
fn new(stride: usize, w: u32, h: u32) -> Items {
|
||||
let (w, h) = (w as i32, h as i32);
|
||||
let tx = (w + TILE - 1) / TILE;
|
||||
let ty = (h + TILE - 1) / TILE;
|
||||
Items { stride, recs: vec![], w, h, tiles: vec![vec![]; (tx * ty).max(0) as usize], tx }
|
||||
}
|
||||
|
||||
/// Add an item (`rec` = its fields, without the bounds) covering float bounds `bb`
|
||||
/// ([x0, y0, x1, y1]).
|
||||
fn push(&mut self, bb: [f64; 4], rec: &[f32]) {
|
||||
if !bb.iter().all(|v| v.is_finite()) {
|
||||
return;
|
||||
}
|
||||
let x0 = bb[0].floor().max(0.0) as i32;
|
||||
let y0 = bb[1].floor().max(0.0) as i32;
|
||||
let x1 = (bb[2].ceil().min(self.w as f64 - 1.0)) as i32;
|
||||
let y1 = (bb[3].ceil().min(self.h as f64 - 1.0)) as i32;
|
||||
if x1 < x0 || y1 < y0 || bb[2] < 0.0 || bb[3] < 0.0 {
|
||||
return;
|
||||
}
|
||||
let i = (self.recs.len() / self.stride) as u32;
|
||||
let start = self.recs.len();
|
||||
self.recs.extend_from_slice(rec);
|
||||
self.recs.resize(start + self.stride - 4, 0.0);
|
||||
self.recs.extend([x0 as f32, y0 as f32, x1 as f32, y1 as f32]);
|
||||
for ty in y0 / TILE..=y1 / TILE {
|
||||
for tx in x0 / TILE..=x1 / TILE {
|
||||
self.tiles[(ty * self.tx + tx) as usize].push(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Kernel data (tile offsets, item indices, items), the items' offset and the tile count
|
||||
/// across. `None` when too large.
|
||||
fn data(self) -> Option<(Vec<f32>, u32, u32)> {
|
||||
let n_tiles = self.tiles.len();
|
||||
let n_idx: usize = self.tiles.iter().map(Vec::len).sum();
|
||||
let total = n_tiles + 1 + n_idx + self.recs.len();
|
||||
if total > MAX_FLOATS {
|
||||
return None;
|
||||
}
|
||||
let mut d = Vec::with_capacity(total);
|
||||
let base = (n_tiles + 1) as f32;
|
||||
let mut at = 0usize;
|
||||
for t in &self.tiles {
|
||||
d.push(base + at as f32);
|
||||
at += t.len();
|
||||
}
|
||||
d.push(base + at as f32);
|
||||
for t in &self.tiles {
|
||||
d.extend(t.iter().map(|&i| i as f32));
|
||||
}
|
||||
let items_at = d.len() as u32;
|
||||
d.extend(self.recs);
|
||||
Some((d, items_at, self.tx.max(0) as u32))
|
||||
}
|
||||
}
|
||||
|
||||
/// A coverage of the segment raster: radius max(r · `mul` + `add`, `min`) and hardness
|
||||
/// (generate3::raster_segs), or Advanced Lightning's linear core.
|
||||
#[derive(Clone, Copy)]
|
||||
struct Variant {
|
||||
mul: f64,
|
||||
add: f64,
|
||||
min: f64,
|
||||
hard: f64,
|
||||
linear: bool,
|
||||
}
|
||||
|
||||
impl Variant {
|
||||
fn plain(hard: f64) -> Variant {
|
||||
Variant { mul: 1.0, add: 0.0, min: NO_MIN as f64, hard, linear: false }
|
||||
}
|
||||
|
||||
fn radius(&self, r: f64) -> f64 {
|
||||
(r * self.mul + self.add).max(self.min)
|
||||
}
|
||||
}
|
||||
|
||||
/// Rasterise segments (a, b, radius, value) into up to four coverages (x, y, z, w), one per
|
||||
/// variant, max-combined as raster_segs.
|
||||
fn raster_segs(e: &mut Enc, w: u32, h: u32, segs: impl Iterator<Item = ([f64; 2], [f64; 2], f64, f32)>, variants: &[Variant]) -> Option<GpuImage> {
|
||||
let mut items = Items::new(10, w, h);
|
||||
for (a, z, r, v) in segs {
|
||||
let pad = variants.iter().map(|k| k.radius(r)).fold(f64::NEG_INFINITY, f64::max) + 1.0;
|
||||
let bb = [a[0].min(z[0]) - pad, a[1].min(z[1]) - pad, a[0].max(z[0]) + pad, a[1].max(z[1]) + pad];
|
||||
items.push(bb, &[a[0] as f32, a[1] as f32, z[0] as f32, z[1] as f32, r as f32, v]);
|
||||
}
|
||||
let (data, items_at, tx) = items.data()?;
|
||||
let buf = e.data(&data);
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [items_at, 10, tx, variants.len() as u32];
|
||||
for (k, v) in variants.iter().enumerate() {
|
||||
p.f[k] = [v.mul as f32, v.add as f32, v.min as f32, v.hard as f32];
|
||||
p.u[1][0] |= (v.linear as u32) << k;
|
||||
}
|
||||
let out = e.scratch(w, h);
|
||||
let dummy = e.zeros(1, 1);
|
||||
e.pixels("fg2_segs", &p, &dummy, None, &out, Some(&buf));
|
||||
Some(out)
|
||||
}
|
||||
|
||||
/// A plan's coverage (x).
|
||||
fn coverage(e: &mut Enc, w: u32, h: u32, cov: &Coverage) -> Option<GpuImage> {
|
||||
match cov {
|
||||
Coverage::Segs { segs, hardness } => raster_segs(e, w, h, segs.iter().map(|s| (s.a, s.b, s.r, s.v)), &[Variant::plain(*hardness)]),
|
||||
Coverage::Convex(polys) => {
|
||||
const STRIDE: usize = 1 + 32 + 4;
|
||||
let mut items = Items::new(STRIDE, w, h);
|
||||
for poly in polys {
|
||||
if poly.len() < 3 {
|
||||
continue;
|
||||
}
|
||||
if poly.len() > 16 {
|
||||
return None;
|
||||
}
|
||||
let mut rec = vec![poly.len() as f32];
|
||||
let mut bb = [f64::INFINITY, f64::INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY];
|
||||
for q in poly {
|
||||
rec.extend([q[0] as f32, q[1] as f32]);
|
||||
bb = [bb[0].min(q[0]), bb[1].min(q[1]), bb[2].max(q[0]), bb[3].max(q[1])];
|
||||
}
|
||||
items.push(bb, &rec);
|
||||
}
|
||||
let (data, items_at, tx) = items.data()?;
|
||||
let buf = e.data(&data);
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [items_at, STRIDE as u32, tx, 0];
|
||||
let out = e.scratch(w, h);
|
||||
let dummy = e.zeros(1, 1);
|
||||
e.pixels("fg2_convex", &p, &dummy, None, &out, Some(&buf));
|
||||
Some(out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Stroke, Scribble, Vegas, Write-on
|
||||
|
||||
fn paint(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let cpu = cpu_buf(e, &b, effectcraft_effects::plan_reads_pixels(id, ctx))?;
|
||||
let Some(plan) = effectcraft_effects::paint_plan(id, ctx, &cpu) else {
|
||||
// Scribble without a usable mask: cleared On Transparent, else unchanged.
|
||||
if id == "ec.generate.scribble" && effectcraft_effects::scribble_style(ctx) == 1 {
|
||||
let img = e.image(b.img.width, b.img.height);
|
||||
return Some(GBuf { img, ..b });
|
||||
}
|
||||
return Some(b);
|
||||
};
|
||||
let cov = coverage(e, b.img.width, b.img.height, &plan.cov)?;
|
||||
let mut p = Params::default();
|
||||
p.u[0][0] = plan.style;
|
||||
p.f[0] = plan.color;
|
||||
p.f[1][0] = plan.opacity;
|
||||
run(e, "fg2_paint", &p, b, Some(&cov), None)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Audio Spectrum / Waveform
|
||||
|
||||
fn marks(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let cpu = cpu_buf(e, &b, false)?;
|
||||
let Some(m) = effectcraft_effects::marks_plan(id, ctx, &cpu) else { return Some(b) };
|
||||
let core = Variant::plain(1.0);
|
||||
let halo = if m.softness > 0.0 { Variant { add: m.thickness * m.softness.max(0.0) + 0.5, ..Variant::plain(0.0) } } else { core };
|
||||
let cov = raster_segs(e, b.img.width, b.img.height, m.segs.iter().map(|s| (s.a, s.b, s.r, s.v)), &[core, halo])?;
|
||||
let mut p = Params::default();
|
||||
p.u[0][0] = m.composite as u32;
|
||||
p.f[0] = m.inside;
|
||||
p.f[1] = m.outside;
|
||||
run(e, "fg2_marks", &p, b, Some(&cov), None)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Lightning / Advanced Lightning
|
||||
|
||||
fn lightning(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let cpu = cpu_buf(e, &b, false)?;
|
||||
let plan = effectcraft_effects::bolt_plan(ctx, &cpu);
|
||||
let (w, h) = (b.img.width, b.img.height);
|
||||
let core = Variant { mul: plan.core, min: 0.3, ..Variant::plain(0.6) };
|
||||
let cov = raster_segs(e, w, h, plan.segs.iter().map(|s| (s.a, s.b, s.r, s.v)), &[Variant::plain(0.0), core])?;
|
||||
let glow = gauss_plane(e, &cov, plan.width * 0.5, plan.width * 0.5);
|
||||
let m = e.scratch(w, h);
|
||||
e.pixels("fg2_pair", &Params::default(), &cov, Some(&glow), &m, None);
|
||||
let mut p = Params::default();
|
||||
p.u[0][0] = plan.mode;
|
||||
p.f[0] = rgb(plan.outside);
|
||||
p.f[1] = rgb(plan.inside);
|
||||
run(e, "fg2_bolt", &p, b, Some(&m), None)
|
||||
}
|
||||
|
||||
fn advanced_lightning(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let pr = ctx.params;
|
||||
let cpu = cpu_buf(e, &b, effectcraft_effects::plan_reads_pixels("ec.generate.advancedlightning", ctx))?;
|
||||
let core_r = (pr.f("coreSettings/coreRadius") * b.scale).max(0.3);
|
||||
let glow_r = pr.f("glowSettings/glowRadius") * b.scale;
|
||||
let segs = effectcraft_effects::lightning_segments(ctx, &cpu);
|
||||
let (w, h) = (b.img.width, b.img.height);
|
||||
let linear = Variant { linear: true, ..Variant::plain(0.0) };
|
||||
let core = raster_segs(e, w, h, segs.iter().map(|(a, z, i)| ([a.0, a.1], [z.0, z.1], core_r, *i)), &[linear])?;
|
||||
let glow = glow_r > 0.5;
|
||||
let blurred = if glow { gauss_plane(e, &core, glow_r / 3.0, glow_r / 3.0) } else { core.clone() };
|
||||
let m = e.scratch(w, h);
|
||||
e.pixels("fg2_pair", &Params::default(), &core, Some(&blurred), &m, None);
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [pr.b("compositeOnOriginal") as u32, glow as u32, 0, 0];
|
||||
p.f[0] = [(pr.f("coreSettings/coreOpacity") / 100.0) as f32, (pr.f("glowSettings/glowOpacity") / 100.0) as f32, 0.0, 0.0];
|
||||
p.f[1] = rgb(pr.color("coreSettings/coreColor"));
|
||||
p.f[2] = rgb(pr.color("glowSettings/glowColor"));
|
||||
run(e, "fg2_adv_bolt", &p, b, Some(&m), None)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Basic Text / Path Text
|
||||
|
||||
fn text(e: &mut Enc, id: &str, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
|
||||
let cpu = cpu_buf(e, &b, false)?;
|
||||
let passes = effectcraft_effects::text_passes(id, ctx, &cpu)?;
|
||||
let (w, h) = (b.img.width, b.img.height);
|
||||
for pass in passes {
|
||||
let look = pass.look;
|
||||
let mut segs = Vec::new();
|
||||
for pl in &pass.polys {
|
||||
effectcraft_effects::poly_segs(pl, false, pass.r, 1.0, &mut segs);
|
||||
}
|
||||
let sw = look.stroke_w.max(0.0);
|
||||
let at = |add: f64| Variant { add, min: 0.05, ..Variant::plain(1.0) };
|
||||
let cov = raster_segs(e, w, h, segs.iter().map(|s| (s.a, s.b, s.r, s.v)), &[at(0.0), at(sw * 0.5), at(-sw * 0.5)])?;
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [look.display, look.on_original as u32, (look.display != 0 && sw > 0.0) as u32, (pass.r - sw * 0.5 > 0.05) as u32];
|
||||
p.f[0] = look.fill;
|
||||
p.f[1] = look.stroke;
|
||||
p.f[2][0] = look.opacity;
|
||||
b = run(e, "fg2_text", &p, b, Some(&cov), None)?;
|
||||
}
|
||||
Some(b)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Radio Waves
|
||||
|
||||
fn radio_waves(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let cpu = cpu_buf(e, &b, false)?;
|
||||
let Some(plan) = effectcraft_effects::radio_plan(ctx, &cpu) else { return Some(b) };
|
||||
let n = plan.waves.len();
|
||||
let mut data = Vec::with_capacity(n * 13);
|
||||
let mut outlines = Vec::new();
|
||||
for wv in &plan.waves {
|
||||
let (off, cnt) = match &wv.outline {
|
||||
Some(o) => {
|
||||
let off = n * 13 + outlines.len();
|
||||
outlines.extend(o.iter().flat_map(|q| [q.0 as f32, q.1 as f32]));
|
||||
(off, o.len())
|
||||
}
|
||||
None => (0, 0),
|
||||
};
|
||||
data.extend([
|
||||
wv.centre.0 as f32,
|
||||
wv.centre.1 as f32,
|
||||
wv.radius as f32,
|
||||
wv.half_width as f32,
|
||||
wv.rotation as f32,
|
||||
wv.fade,
|
||||
wv.color[0],
|
||||
wv.color[1],
|
||||
wv.color[2],
|
||||
wv.color[3],
|
||||
wv.opacity,
|
||||
off as f32,
|
||||
cnt as f32,
|
||||
]);
|
||||
}
|
||||
data.extend(outlines);
|
||||
if data.len() > MAX_FLOATS {
|
||||
return None;
|
||||
}
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [n as u32, plan.contour.is_some() as u32, plan.profile, 0];
|
||||
p.f[0][0] = plan.aa as f32;
|
||||
let sdf = match &plan.contour {
|
||||
Some(c) => {
|
||||
let mut img = Image::new(c.sdf.w as u32, c.sdf.h as u32);
|
||||
for (px, &v) in img.data.iter_mut().zip(&c.sdf.data) {
|
||||
px[0] = v;
|
||||
}
|
||||
p.f[0][1] = c.anchor.0 as f32;
|
||||
p.f[0][2] = c.anchor.1 as f32;
|
||||
Some(e.g.upload_image(&img)?)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
let buf = e.data(&data);
|
||||
run(e, "fg2_radio", &p, b, sdf.as_ref(), Some(&buf))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Beam / Lens Flare
|
||||
|
||||
fn beam(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let pr = ctx.params;
|
||||
let s = b.to_px(pr.v2("startPoint"));
|
||||
let en = b.to_px(pr.v2("endPoint"));
|
||||
let len = (pr.f("length") / 100.0).clamp(0.0, 1.0);
|
||||
let time = (pr.f("time") / 100.0).clamp(0.0, 1.0);
|
||||
let t0 = time * (1.0 - len);
|
||||
let (dx, dy) = (en.0 - s.0, en.1 - s.1);
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [pr.b("perspective3d") as u32, pr.b("compositeOnOriginal") as u32, (len > 0.0) as u32, 0];
|
||||
p.f[0] = [s.0 as f32, s.1 as f32, dx as f32, dy as f32];
|
||||
p.f[1] = [t0 as f32, (t0 + len) as f32, (pr.f("startThickness") * b.scale) as f32, (pr.f("endThickness") * b.scale) as f32];
|
||||
p.f[2] = [(pr.f("softness") / 100.0).clamp(0.0, 1.0) as f32, (dx * dx + dy * dy).max(1e-9) as f32, 0.0, 0.0];
|
||||
p.f[3] = rgb(pr.color("insideColor"));
|
||||
p.f[4] = rgb(pr.color("outsideColor"));
|
||||
run(e, "fg2_beam", &p, b, None, None)
|
||||
}
|
||||
|
||||
fn lens_flare(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let pr = ctx.params;
|
||||
let f = b.to_px(pr.v2("flareCenter"));
|
||||
let lens = pr.e("lensType");
|
||||
let (w, h) = (ctx.layer_size[0] * b.scale, ctx.layer_size[1] * b.scale);
|
||||
let c = (b.offset[0] + w * 0.5, b.offset[1] + h * 0.5);
|
||||
let diag = (w * w + h * h).sqrt().max(1.0);
|
||||
let (core_k, rays, halo_r) = match lens {
|
||||
1 => (0.035, 6.0, 0.18),
|
||||
2 => (0.02, 12.0, 0.3),
|
||||
_ => (0.03, 8.0, 0.25),
|
||||
};
|
||||
let ghosts = effectcraft_effects::flare_ghosts(lens);
|
||||
let data: Vec<f32> = ghosts.iter().flat_map(|g| [g.t as f32, g.r as f32, g.c[0], g.c[1], g.c[2], g.k, g.ring as u32 as f32]).collect();
|
||||
let mut p = Params::default();
|
||||
p.u[0][0] = ghosts.len() as u32;
|
||||
p.f[0] = [f.0 as f32, f.1 as f32, c.0 as f32, c.1 as f32];
|
||||
p.f[1] = [diag as f32, core_k, rays, halo_r];
|
||||
p.f[2] = [(pr.f("flareBrightness") / 100.0) as f32, (pr.f("blendWithOriginal") / 100.0) as f32, 0.0, 0.0];
|
||||
let buf = e.data(&data);
|
||||
run(e, "fg2_flare", &p, b, None, Some(&buf))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Glue Gun / CC Threads
|
||||
|
||||
fn glue_gun(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let pr = ctx.params;
|
||||
let q = b.to_px(pr.v2("brushPosition"));
|
||||
let rad = (pr.f("strokeWidth") * b.scale * 0.5).max(0.5);
|
||||
let refl = pr.f("reflection").to_radians();
|
||||
let strength = (pr.f("strength") / 100.0) as f32;
|
||||
let beads = (pr.f("density").max(0.0).round() as usize).clamp(1, 32);
|
||||
let hash = |i: usize, k: u32| effectcraft_raster::hash_noise(i as u32, k, ctx.seed) as f64;
|
||||
// generate3::glue_gun's beads.
|
||||
let mut data = Vec::with_capacity(beads * 3);
|
||||
for i in 0..beads {
|
||||
let a = hash(i, 1) * 2.0 * std::f64::consts::PI;
|
||||
let d = if i == 0 { 0.0 } else { hash(i, 2) * rad * 0.35 };
|
||||
data.extend([(q.0 + a.cos() * d) as f32, (q.1 + a.sin() * d) as f32, (rad * (0.75 + 0.25 * hash(i, 3))) as f32]);
|
||||
}
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [beads as u32, (pr.e("style") == 1) as u32, 0, 0];
|
||||
p.f[0] = [(refl.cos() * 0.6) as f32, (-refl.sin() * 0.6) as f32, 0.53, strength];
|
||||
p.f[1][0] = strength.clamp(0.0, 1.0).max(0.3);
|
||||
let buf = e.data(&data);
|
||||
run(e, "fg2_glue", &p, b, None, Some(&buf))
|
||||
}
|
||||
|
||||
fn threads(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let pr = ctx.params;
|
||||
let sc = b.scale;
|
||||
let (sn, cs) = pr.f("direction").to_radians().sin_cos();
|
||||
let c = b.to_px(pr.v2("center"));
|
||||
let mut p = Params::default();
|
||||
p.u[0][0] = pr.f("overlaps").round().clamp(1.0, 20.0) as u32;
|
||||
p.f[0] = [c.0 as f32, c.1 as f32, sn as f32, cs as f32];
|
||||
p.f[1] = [
|
||||
(pr.f("width") * sc).max(1.0) as f32,
|
||||
(pr.f("height") * sc).max(1.0) as f32,
|
||||
(pr.f("coverage") / 100.0).clamp(0.0, 1.0) as f32,
|
||||
(pr.f("shadowing") / 100.0) as f32,
|
||||
];
|
||||
p.f[2][0] = (pr.f("texture") / 100.0) as f32;
|
||||
run(e, "fg2_threads", &p, b, None, None)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Paint Bucket / Eyedropper Fill
|
||||
|
||||
fn paint_bucket(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let pr = ctx.params;
|
||||
let cpu = cpu_buf(e, &b, true)?;
|
||||
let region = effectcraft_effects::paint_bucket_region(ctx, &cpu);
|
||||
let (w, h) = (b.img.width, b.img.height);
|
||||
let mut img = Image::new(w, h);
|
||||
for (px, &v) in img.data.iter_mut().zip(®ion.data) {
|
||||
px[0] = v;
|
||||
}
|
||||
let region = e.g.upload_image(&img)?;
|
||||
let cov_kernel = |e: &mut Enc, mode: u32, a: &GpuImage, x: &GpuImage| {
|
||||
let mut p = Params::default();
|
||||
p.u[0][0] = mode;
|
||||
let out = e.scratch(w, h);
|
||||
e.pixels("fg2_bucket_cov", &p, a, Some(x), &out, None);
|
||||
out
|
||||
};
|
||||
if pr.b("viewThreshold") {
|
||||
let img = cov_kernel(e, 1, ®ion, ®ion);
|
||||
return Some(GBuf { img, ..b });
|
||||
}
|
||||
let sc = b.scale;
|
||||
use crate::fx_color::{DILATE_X, ERODE_X, morph_frac};
|
||||
let cov = match pr.e("stroke") {
|
||||
1 => {
|
||||
let s = pr.f("featherSoftness") * sc * 0.5;
|
||||
if s > 0.05 { gauss_plane(e, ®ion, s, s) } else { region }
|
||||
}
|
||||
2 => morph_frac(e, ®ion, pr.f("spreadRadius") * sc, DILATE_X),
|
||||
3 => morph_frac(e, ®ion, pr.f("spreadRadius") * sc, ERODE_X),
|
||||
4 => {
|
||||
let r = pr.f("strokeWidth") * sc;
|
||||
let out = morph_frac(e, ®ion, r * 0.5, DILATE_X);
|
||||
let inn = morph_frac(e, ®ion, r * 0.5, ERODE_X);
|
||||
cov_kernel(e, 2, &out, &inn)
|
||||
}
|
||||
_ => {
|
||||
let blurred = gauss_plane(e, ®ion, 0.5, 0.5);
|
||||
cov_kernel(e, 0, ®ion, &blurred)
|
||||
}
|
||||
};
|
||||
let mut p = Params::default();
|
||||
p.u[0][0] = pr.e("blendingMode");
|
||||
p.f[0] = pr.color("color");
|
||||
p.f[1][0] = (pr.f("opacity") / 100.0) as f32;
|
||||
run(e, "fg2_bucket", &p, b, Some(&cov), None)
|
||||
}
|
||||
|
||||
fn eyedropper(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let cpu = cpu_buf(e, &b, true)?;
|
||||
let fill = effectcraft_effects::eyedropper_color(ctx, &cpu);
|
||||
let mut p = Params::default();
|
||||
p.u[0][0] = ctx.params.b("maintainOriginalAlpha") as u32;
|
||||
p.f[0] = fill;
|
||||
p.f[1][0] = (ctx.params.f("blendWithOriginal") / 100.0) as f32;
|
||||
run(e, "fg2_eyedropper", &p, b, None, None)
|
||||
}
|
||||
@@ -88,7 +88,7 @@ fn src_index(s: Src) -> u32 {
|
||||
|
||||
/// Layer parameter `id` resampled into the buffer's pixel grid (`util::fit_layer`):
|
||||
/// `Some(None)` when no layer is chosen (the effect then uses its own layer).
|
||||
fn fitted(e: &mut Enc, ctx: &EffectCtx, b: &GBuf, id: &str, masks_and_effects: bool, stretch: bool) -> Option<Option<GpuImage>> {
|
||||
pub(crate) fn fitted(e: &mut Enc, ctx: &EffectCtx, b: &GBuf, id: &str, masks_and_effects: bool, stretch: bool) -> Option<Option<GpuImage>> {
|
||||
let Some(o) = ctx.layer_param(id, masks_and_effects) else { return Some(None) };
|
||||
let (w, h) = (b.img.width, b.img.height);
|
||||
if o.buf.img.is_empty() {
|
||||
|
||||
@@ -0,0 +1,518 @@
|
||||
//! GPU effects, pixel ports of part C (kernels in `shaders/fx_pixel2.wgsl`, every entry point
|
||||
//! prefixed `fp2_`): CC Cross Blur, CC Radial Blur, CC Vector Blur, Reduce Interlace Flicker,
|
||||
//! CC Composite, Color Link, Cineon Converter, HDR Compander, HDR Highlight Compression, Grow
|
||||
//! Bounds, CC Overbrights, CC Block Load, CC Burn Film, CC Glass, CC HexTile, CC Mr. Smoothie,
|
||||
//! CC Plastic, Inner/Outer Key, CC Simple Wire Removal and Basic 3D, with the CPU effects'
|
||||
//! exact steps (padding, box-blur radii, parameter conversions in f64 on the CPU).
|
||||
//!
|
||||
//! What only depends on parameters and geometry is prepared on the CPU and shared with the CPU
|
||||
//! effect: Inner/Outer Key's trimap and Cleanup strokes (`effects::inner_outer_plan`), CC Glass /
|
||||
//! Plastic's light (`effects::BumpLight`). Color Link's sample statistics (sorting, trimmed
|
||||
//! means) run on the CPU over the read-back layer (`effects::color_link_sample`) unless a Source
|
||||
//! Layer is chosen; the colouring runs here.
|
||||
|
||||
use effectcraft_color::BlendMode;
|
||||
use effectcraft_effects::EffectCtx;
|
||||
use effectcraft_raster::Image;
|
||||
|
||||
use crate::context::{Enc, GpuImage, Params};
|
||||
use crate::effects::{GBuf, box_passes, gaussian_blur};
|
||||
use crate::ops::mode_id;
|
||||
|
||||
/// Compute entry points in `fx_pixel2.wgsl`.
|
||||
pub(crate) const KERNELS: &[&str] = &[
|
||||
"fp2_cross",
|
||||
"fp2_box",
|
||||
"fp2_radial",
|
||||
"fp2_plane",
|
||||
"fp2_vector",
|
||||
"fp2_point",
|
||||
"fp2_block_means",
|
||||
"fp2_block_load",
|
||||
"fp2_burn",
|
||||
"fp2_bump",
|
||||
"fp2_hextile",
|
||||
"fp2_smoothie",
|
||||
"fp2_io_prep",
|
||||
"fp2_io_known",
|
||||
"fp2_io_est",
|
||||
"fp2_io_strokes",
|
||||
"fp2_io_final",
|
||||
"fp2_wire",
|
||||
"fp2_basic3d",
|
||||
];
|
||||
|
||||
/// Effect ids implemented here.
|
||||
pub(crate) const IDS: &[&str] = &[
|
||||
"ec.blur.cccross",
|
||||
"ec.blur.ccradial",
|
||||
"ec.blur.ccvector",
|
||||
"ec.blur.reduceflicker",
|
||||
"ec.channel.cccomposite",
|
||||
"ec.color.colorlink",
|
||||
"ec.utility.cineon",
|
||||
"ec.utility.hdrcompander",
|
||||
"ec.utility.hdrcompression",
|
||||
"ec.utility.growbounds",
|
||||
"ec.utility.ccoverbrights",
|
||||
"ec.stylize.ccblockload",
|
||||
"ec.stylize.ccburnfilm",
|
||||
"ec.stylize.ccglass",
|
||||
"ec.stylize.cchextile",
|
||||
"ec.stylize.ccmrsmoothie",
|
||||
"ec.stylize.ccplastic",
|
||||
"ec.key.innerouter",
|
||||
"ec.key.ccsimplewireremoval",
|
||||
"ec.obsolete.basic3d",
|
||||
];
|
||||
|
||||
/// Run effect `id` (one of [`IDS`]); `None` = this parameter combination runs on the CPU.
|
||||
pub(crate) fn apply(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
match id {
|
||||
"ec.blur.cccross" => cross_blur(e, ctx, b),
|
||||
"ec.blur.ccradial" => radial_blur(e, ctx, b),
|
||||
"ec.blur.ccvector" => vector_blur(e, ctx, b),
|
||||
"ec.blur.reduceflicker" => reduce_flicker(e, ctx, b),
|
||||
"ec.utility.growbounds" => grow_bounds(e, ctx, b),
|
||||
"ec.stylize.ccblockload" => block_load(e, ctx, b),
|
||||
"ec.stylize.ccburnfilm" => burn_film(e, ctx, b),
|
||||
"ec.stylize.ccglass" | "ec.stylize.ccplastic" => bump(e, id, ctx, b),
|
||||
"ec.stylize.cchextile" => hextile(e, ctx, b),
|
||||
"ec.stylize.ccmrsmoothie" => smoothie(e, ctx, b),
|
||||
"ec.key.innerouter" => inner_outer(e, ctx, b),
|
||||
"ec.key.ccsimplewireremoval" => wire_removal(e, ctx, b),
|
||||
"ec.obsolete.basic3d" => basic_3d(e, ctx, b),
|
||||
_ => point(e, id, ctx, b),
|
||||
}
|
||||
}
|
||||
|
||||
/// A same-size per-pixel kernel over the buffer.
|
||||
fn run(e: &mut Enc, entry: &str, p: &Params, mut b: GBuf, aux: Option<&GpuImage>, data: Option<&wgpu::Buffer>) -> Option<GBuf> {
|
||||
let out = e.scratch(b.img.width, b.img.height);
|
||||
e.pixels(entry, p, &b.img, aux, &out, data);
|
||||
b.img = out;
|
||||
Some(b)
|
||||
}
|
||||
|
||||
/// A one-value plane (x) of `img` (`fp2_plane` kind / property), softened by util::gauss_plane.
|
||||
fn plane(e: &mut Enc, img: &GpuImage, kind: u32, prop: u32, soft: f64) -> GpuImage {
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [kind, prop, 0, 0];
|
||||
let pl = e.scratch(img.width, img.height);
|
||||
e.pixels("fp2_plane", &p, img, None, &pl, None);
|
||||
gauss_plane(e, &pl, soft, soft)
|
||||
}
|
||||
|
||||
/// util::gauss_plane on every channel: 3 box passes per axis (edges repeated), each window
|
||||
/// summed directly (the CPU sums in f64; see `fp2_box`).
|
||||
pub(crate) fn gauss_plane(e: &mut Enc, img: &GpuImage, sx: f64, sy: f64) -> GpuImage {
|
||||
let mut cur = img.clone();
|
||||
for (vertical, sigma) in [(0, sx), (1, sy)] {
|
||||
if sigma <= 0.05 {
|
||||
continue;
|
||||
}
|
||||
for r in crate::effects::box_radii(sigma, 3) {
|
||||
if r == 0 {
|
||||
continue;
|
||||
}
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [r as u32, vertical, 0, 0];
|
||||
let out = e.scratch(cur.width, cur.height);
|
||||
e.pixels("fp2_box", &p, &cur, None, &out, None);
|
||||
cur = out;
|
||||
}
|
||||
}
|
||||
cur
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- blurs (blur2, blur3)
|
||||
|
||||
fn cross_blur(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
|
||||
let rx = (ctx.params.f("radiusX").max(0.0) * b.scale).round() as usize;
|
||||
let ry = (ctx.params.f("radiusY").max(0.0) * b.scale).round() as usize;
|
||||
if rx == 0 && ry == 0 {
|
||||
return Some(b);
|
||||
}
|
||||
if !ctx.adjustment {
|
||||
b.pad(e, (rx.max(ry) * 3) as u32 + 1)?;
|
||||
}
|
||||
let repeat = ctx.adjustment;
|
||||
let hz = box_passes(e, &b.img, &[rx; 3], &[], repeat);
|
||||
let vt = box_passes(e, &b.img, &[], &[ry; 3], repeat);
|
||||
let mut p = Params::default();
|
||||
p.u[0][0] = ctx.params.e("transferMode");
|
||||
let out = e.scratch(b.img.width, b.img.height);
|
||||
e.pixels("fp2_cross", &p, &hz, Some(&vt), &out, None);
|
||||
b.img = out;
|
||||
Some(b)
|
||||
}
|
||||
|
||||
fn radial_blur(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let amount = ctx.params.f("amount");
|
||||
if amount == 0.0 {
|
||||
return Some(b);
|
||||
}
|
||||
let c = b.to_px(ctx.params.v2("center"));
|
||||
let mut p = Params::default();
|
||||
p.u[0][0] = ctx.params.e("type");
|
||||
p.f[0] = [c.0 as f32, c.1 as f32, ((amount / 100.0).clamp(-1.0, 1.0) * 0.5) as f32, (amount.to_radians() * 0.25) as f32];
|
||||
p.f[1][0] = ctx.params.f("quality").clamp(1.0, 100.0) as f32;
|
||||
run(e, "fp2_radial", &p, b, None, None)
|
||||
}
|
||||
|
||||
fn vector_blur(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let amount = ctx.params.f("amount").max(0.0) * b.scale;
|
||||
if amount < 0.5 {
|
||||
return Some(b);
|
||||
}
|
||||
let soft = ctx.params.f("mapSoftness").max(0.0) * 0.5 * b.scale;
|
||||
let map = plane(e, &b.img, 0, 0, soft);
|
||||
let mut p = Params::default();
|
||||
p.u[0][0] = ctx.params.e("type");
|
||||
p.f[0] = [amount as f32, ctx.params.f("angleOffset").to_radians() as f32, 0.0, 0.0];
|
||||
run(e, "fp2_vector", &p, b, Some(&map), None)
|
||||
}
|
||||
|
||||
fn reduce_flicker(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
|
||||
let s = ctx.params.f("softness").max(0.0) * b.scale;
|
||||
if s < 0.05 {
|
||||
return Some(b);
|
||||
}
|
||||
b.img = gaussian_blur(e, &b.img, 0.0, s, true);
|
||||
Some(b)
|
||||
}
|
||||
|
||||
fn grow_bounds(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
|
||||
let px = (ctx.params.f("pixels") * b.scale).round().max(0.0) as u32;
|
||||
if !ctx.adjustment {
|
||||
b.pad(e, px)?;
|
||||
}
|
||||
Some(b)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- per-pixel colour effects
|
||||
|
||||
/// channel2::cc_composite's Composite Original modes.
|
||||
fn composite_mode(m: u32) -> BlendMode {
|
||||
match m {
|
||||
2 => BlendMode::Add,
|
||||
3 => BlendMode::Multiply,
|
||||
4 => BlendMode::Screen,
|
||||
5 => BlendMode::Overlay,
|
||||
6 => BlendMode::SoftLight,
|
||||
7 => BlendMode::HardLight,
|
||||
8 => BlendMode::Darken,
|
||||
9 => BlendMode::Lighten,
|
||||
10 => BlendMode::Difference,
|
||||
11 => BlendMode::Hue,
|
||||
12 => BlendMode::Saturation,
|
||||
13 => BlendMode::Color,
|
||||
14 => BlendMode::Luminosity,
|
||||
15 => BlendMode::StencilAlpha,
|
||||
_ => BlendMode::Normal,
|
||||
}
|
||||
}
|
||||
|
||||
fn point(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let pr = ctx.params;
|
||||
let mut p = Params::default();
|
||||
match id {
|
||||
"ec.utility.cineon" => {
|
||||
// utility::Cineon's offset and knee, in the CPU's f32.
|
||||
let black = pr.f("tenBitBlackPoint") as f32;
|
||||
let white = pr.f("tenBitWhitePoint") as f32;
|
||||
let rolloff = pr.f("highlightRolloff") as f32;
|
||||
let offset = |black: f32, white: f32| 10f32.powf((black - white) * 0.002 / 0.6);
|
||||
let knee = 1.0 - (rolloff / 100.0).clamp(0.0, 0.99);
|
||||
p.u[0] = [0, pr.e("conversionType"), 0, 0];
|
||||
p.f[0] = [offset(black, white), knee, white, pr.f("gamma") as f32];
|
||||
p.f[1] = [pr.f("internalBlackPoint") as f32, pr.f("internalWhitePoint") as f32, 0.0, 0.0];
|
||||
p.f[2] = [offset(95.0, 685.0), knee, 685.0, 1.7];
|
||||
}
|
||||
"ec.utility.hdrcompander" => {
|
||||
p.u[0] = [1, (pr.e("mode") == 1) as u32, 0, 0];
|
||||
p.f[0] = [pr.f("gain").max(1e-6) as f32, pr.f("gamma").max(1e-3) as f32, 0.0, 0.0];
|
||||
}
|
||||
"ec.utility.hdrcompression" => {
|
||||
let t = (pr.f("amount") / 100.0).clamp(0.0, 1.0) as f32;
|
||||
if t <= 0.0 {
|
||||
return Some(b);
|
||||
}
|
||||
p.u[0][0] = 2;
|
||||
p.f[0][0] = 1.0 - 0.5 * t;
|
||||
}
|
||||
"ec.utility.ccoverbrights" => {
|
||||
p.u[0] = [3, pr.e("channel"), 0, 0];
|
||||
p.f[0] = pr.color("highlightColor");
|
||||
}
|
||||
"ec.channel.cccomposite" => {
|
||||
let m = pr.e("compositeOriginal");
|
||||
let mode = if m == 1 { BlendMode::Normal } else { composite_mode(m) };
|
||||
p.u[0] = [6, (m == 1) as u32, mode_id(mode), pr.b("rgbOnly") as u32];
|
||||
p.f[0][0] = (pr.f("opacity") as f32 / 100.0).clamp(0.0, 1.0);
|
||||
}
|
||||
"ec.color.colorlink" => {
|
||||
// The sample statistics run on the CPU (the layer's own pixels are read back).
|
||||
let own = if ctx.layer_param("sourceLayer", false).is_none() { Some(e.download(&b.img)?) } else { None };
|
||||
let (c, sa) = effectcraft_effects::color_link_sample(ctx, own.as_ref())?;
|
||||
let op = (pr.f("opacity") / 100.0).clamp(0.0, 1.0) as f32;
|
||||
let stencil = pr.b("stencilOriginalAlpha") as u32;
|
||||
if pr.e("sampleSource") >= 6 {
|
||||
p.u[0] = [4, stencil, 0, 0];
|
||||
p.f[0] = [sa, op, 0.0, 0.0];
|
||||
} else {
|
||||
p.u[0] = [5, stencil, mode_id(effectcraft_effects::color_link_mode(ctx)), 0];
|
||||
p.f[0] = [c[0] * op, c[1] * op, c[2] * op, op];
|
||||
}
|
||||
}
|
||||
_ => return None,
|
||||
}
|
||||
run(e, "fp2_point", &p, b, None, None)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Block Load (stylize3)
|
||||
|
||||
fn block_load(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
|
||||
let c = (ctx.params.f("completion") / 100.0).clamp(0.0, 1.0);
|
||||
if c >= 1.0 {
|
||||
return Some(b);
|
||||
}
|
||||
let (w, h) = (b.img.width as usize, b.img.height as usize);
|
||||
let nlev = (w.max(h).max(2) as f64).log2().ceil() as u32;
|
||||
let lp = c * (nlev + 1) as f64;
|
||||
let done = lp.floor() as u32;
|
||||
let frac = (lp - done as f64) as f32;
|
||||
let size = |k: u32| 1usize << (nlev - k.min(nlev));
|
||||
let means = |e: &mut Enc, s: usize| {
|
||||
let mut p = Params::default();
|
||||
p.u[0][0] = s as u32;
|
||||
let g = e.scratch(w.div_ceil(s).max(1) as u32, h.div_ceil(s).max(1) as u32);
|
||||
e.pixels("fp2_block_means", &p, &b.img, None, &g, None);
|
||||
g
|
||||
};
|
||||
let s_new = size(done);
|
||||
let new = means(e, s_new);
|
||||
let old = (done > 0).then(|| (size(done - 1), means(e, size(done - 1))));
|
||||
let flags = ctx.params.b("scanlines") as u32
|
||||
| (ctx.params.b("smoothing") as u32) << 1
|
||||
| (ctx.params.b("bilinear") as u32) << 2
|
||||
| (old.is_some() as u32) << 3
|
||||
| (ctx.params.b("startCleared") as u32) << 4;
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [s_new as u32, old.as_ref().map_or(1, |o| o.0 as u32), flags, 0];
|
||||
p.f[0] = [frac, h.div_ceil(s_new) as f32, 0.0, 0.0];
|
||||
let out = e.scratch(w as u32, h as u32);
|
||||
e.pixels("fp2_block_load", &p, &new, old.as_ref().map(|o| &o.1), &out, None);
|
||||
b.img = out;
|
||||
Some(b)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Burn Film (stylize3)
|
||||
|
||||
fn burn_film(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let burn = (ctx.params.f("burn") / 100.0).clamp(0.0, 1.0) as f32;
|
||||
if burn <= 0.0 {
|
||||
return Some(b);
|
||||
}
|
||||
let c = b.to_px(ctx.params.v2("center"));
|
||||
let seed = ctx.params.f("randomSeed").max(0.0) as u32;
|
||||
let s = b.scale;
|
||||
let diag = (ctx.layer_size[0].hypot(ctx.layer_size[1]) * s).max(1.0);
|
||||
let feat = (60.0 * s).max(1e-3);
|
||||
let mut p = Params::default();
|
||||
p.u[0][0] = seed.wrapping_add(17);
|
||||
p.f[0] = [c.0 as f32, c.1 as f32, diag as f32, feat as f32];
|
||||
p.f[1] = [1.0 - burn * 1.25, 0.12 * (burn * 10.0).min(1.0), 0.0, 0.0];
|
||||
run(e, "fp2_burn", &p, b, None, None)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Glass / CC Plastic (stylize3)
|
||||
|
||||
fn bump(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let glass = id == "ec.stylize.ccglass";
|
||||
// stylize3::height_field.
|
||||
let src = crate::fx_key::fitted(e, ctx, &b, effectcraft_effects::bump_layer_id(ctx), true, false)?.unwrap_or_else(|| b.img.clone());
|
||||
let soft = ctx.params.f("softness") * b.scale * 0.5;
|
||||
let mut hf = plane(e, &src, 1, ctx.params.e("property"), soft);
|
||||
let height = ctx.params.f("height") as f32 / 100.0;
|
||||
let mut disp = 0.0;
|
||||
if glass {
|
||||
disp = (ctx.params.f("displacement") * b.scale * 0.25) as f32;
|
||||
} else {
|
||||
let lo = (ctx.params.f("cutMin") / 100.0) as f32;
|
||||
let hi = (ctx.params.f("cutMax") / 100.0) as f32;
|
||||
if lo > 0.0 || hi < 1.0 {
|
||||
hf = clamp_plane(e, &hf, lo.min(hi), hi.max(lo));
|
||||
}
|
||||
}
|
||||
let l = effectcraft_effects::BumpLight::from(ctx, &geo(&b));
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [glass as u32, l.point as u32, 0, 0];
|
||||
p.f[0] = [height * 25.0, disp, (height != 0.0) as u32 as f32, l.height];
|
||||
p.f[1] = [l.intensity, l.ambient, l.diffuse, l.specular];
|
||||
p.f[2] = [l.shininess, l.metal, l.pos.0 as f32, l.pos.1 as f32];
|
||||
p.f[3] = [l.color[0], l.color[1], l.color[2], 0.0];
|
||||
p.f[4] = [l.dir[0], l.dir[1], l.dir[2], 0.0];
|
||||
run(e, "fp2_bump", &p, b, Some(&hf), None)
|
||||
}
|
||||
|
||||
/// An empty CPU buffer with `b`'s geometry (for CPU helpers that only read positions).
|
||||
fn geo(b: &GBuf) -> effectcraft_effects::Buf {
|
||||
effectcraft_effects::Buf { img: Image { width: b.img.width, height: b.img.height, data: vec![] }, offset: b.offset, scale: b.scale }
|
||||
}
|
||||
|
||||
/// Clamp a plane (x) into [lo, hi] (Plane::map with f32::clamp).
|
||||
fn clamp_plane(e: &mut Enc, pl: &GpuImage, lo: f32, hi: f32) -> GpuImage {
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [3, 0, 0, 0];
|
||||
p.f[0] = [lo, hi, 0.0, 0.0];
|
||||
let out = e.scratch(pl.width, pl.height);
|
||||
e.pixels("fp2_plane", &p, pl, None, &out, None);
|
||||
out
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC HexTile / CC Mr. Smoothie (stylize3)
|
||||
|
||||
fn hextile(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let r = (ctx.params.f("radius") * b.scale).max(0.5);
|
||||
let c = b.to_px(ctx.params.v2("center"));
|
||||
let (sn, cs) = ctx.params.f("rotate").to_radians().sin_cos();
|
||||
let mut p = Params::default();
|
||||
p.u[0][0] = ctx.params.e("render");
|
||||
p.f[0] = [c.0 as f32, c.1 as f32, r as f32, (ctx.params.f("smearing") / 100.0).clamp(0.0, 1.0) as f32];
|
||||
p.f[1] = [sn as f32, cs as f32, 0.0, 0.0];
|
||||
run(e, "fp2_hextile", &p, b, None, None)
|
||||
}
|
||||
|
||||
fn smoothie(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let flow = crate::fx_key::fitted(e, ctx, &b, "flowLayer", true, false)?.unwrap_or_else(|| b.img.clone());
|
||||
let sm = ctx.params.f("smoothness") * b.scale * 0.5;
|
||||
let pl = plane(e, &flow, 2, ctx.params.e("property"), sm);
|
||||
let a = b.to_px(ctx.params.v2("sampleA"));
|
||||
let bb = b.to_px(ctx.params.v2("sampleB"));
|
||||
let mut p = Params::default();
|
||||
p.f[0] = [a.0 as f32, a.1 as f32, bb.0 as f32, bb.1 as f32];
|
||||
p.f[1] = [(ctx.params.f("phase") / 360.0) as f32, (ctx.params.e("colorLoop") + 1) as f32, 0.0, 0.0];
|
||||
run(e, "fp2_smoothie", &p, b, Some(&pl), None)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Inner/Outer Key (keying2)
|
||||
|
||||
fn inner_outer(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let Some(plan) = effectcraft_effects::inner_outer_plan(ctx, &geo(&b)) else { return Some(b) };
|
||||
let (w, h) = (b.img.width, b.img.height);
|
||||
let mut tri = Image::new(w, h);
|
||||
for (px, &t) in tri.data.iter_mut().zip(&plan.tri.data) {
|
||||
px[0] = t;
|
||||
}
|
||||
let tri = e.g.upload_image(&tri)?;
|
||||
// The matte (x, or w of the estimate) and the estimate's colours.
|
||||
let (mut matte, est) = if plan.unknown {
|
||||
let t = e.scratch(w, h);
|
||||
e.pixels("fp2_io_prep", &Params::default(), &b.img, Some(&tri), &t, None);
|
||||
let known = |e: &mut Enc, side: u32| {
|
||||
let mut p = Params::default();
|
||||
p.u[0][0] = side;
|
||||
let k = e.scratch(w, h);
|
||||
e.pixels("fp2_io_known", &p, &t, None, &k, None);
|
||||
gauss_plane(e, &k, plan.sigma, plan.sigma)
|
||||
};
|
||||
let fg = known(e, 0);
|
||||
let bg = known(e, 1);
|
||||
let (rows, stride) = e.image_rows(&t);
|
||||
let mut p = Params::default();
|
||||
p.u[0][0] = stride;
|
||||
let est = e.scratch(w, h);
|
||||
e.pixels("fp2_io_est", &p, &fg, Some(&bg), &est, Some(&rows));
|
||||
(est.clone(), Some(est))
|
||||
} else {
|
||||
(tri, None)
|
||||
};
|
||||
let from_w = est.is_some() as u32;
|
||||
if !plan.strokes.is_empty() || from_w != 0 {
|
||||
let n = plan.strokes.len();
|
||||
let mut data = Vec::with_capacity(n + n * (w * h) as usize);
|
||||
data.extend(plan.strokes.iter().map(|s| s.0));
|
||||
for (_, cov) in &plan.strokes {
|
||||
data.extend_from_slice(cov);
|
||||
}
|
||||
let buf = e.data(&data);
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [n as u32, from_w, 0, 0];
|
||||
let out = e.scratch(w, h);
|
||||
e.pixels("fp2_io_strokes", &p, &matte, None, &out, Some(&buf));
|
||||
matte = out;
|
||||
}
|
||||
if plan.thin.abs() > 0.01 {
|
||||
let modes = if plan.thin < 0.0 { crate::fx_color::DILATE_X } else { crate::fx_color::ERODE_X };
|
||||
matte = crate::fx_color::morph_frac(e, &matte, plan.thin.abs(), modes);
|
||||
}
|
||||
if plan.feather > 0.05 {
|
||||
matte = gauss_plane(e, &matte, plan.feather * 0.5, plan.feather * 0.5);
|
||||
}
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [0, from_w, plan.invert as u32, 0];
|
||||
p.f[0] = [plan.threshold, plan.original, 0.0, 0.0];
|
||||
match est {
|
||||
Some(est) => {
|
||||
let (rows, stride) = e.image_rows(&est);
|
||||
p.u[0][0] = stride;
|
||||
run(e, "fp2_io_final", &p, b, Some(&matte), Some(&rows))
|
||||
}
|
||||
None => run(e, "fp2_io_final", &p, b, Some(&matte), None),
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Simple Wire Removal (keying2)
|
||||
|
||||
fn wire_removal(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let half = (ctx.params.f("thickness") * b.scale * 0.5).max(0.0);
|
||||
if half <= 0.0 {
|
||||
return Some(b);
|
||||
}
|
||||
let slope = (ctx.params.f("slope") / 100.0).clamp(0.0, 1.0) as f32;
|
||||
let mirror = (ctx.params.f("mirrorBlend") / 100.0).clamp(0.0, 1.0) as f32;
|
||||
let a = b.to_px(ctx.params.v2("pointA"));
|
||||
let z = b.to_px(ctx.params.v2("pointB"));
|
||||
let (dx, dy) = (z.0 - a.0, z.1 - a.1);
|
||||
let len = (dx * dx + dy * dy).sqrt();
|
||||
if len < 1e-6 {
|
||||
return Some(b);
|
||||
}
|
||||
let (tx, ty) = (dx / len, dy / len);
|
||||
let mut p = Params::default();
|
||||
p.u[0][0] = ctx.params.e("removalStyle");
|
||||
p.f[0] = [a.0 as f32, a.1 as f32, -ty as f32, tx as f32];
|
||||
p.f[1] = [half as f32, (half * slope as f64) as f32, mirror, 0.0];
|
||||
run(e, "fp2_wire", &p, b, None, None)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Basic 3D (obsolete)
|
||||
|
||||
fn basic_3d(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let swivel = ctx.params.f("swivel").to_radians();
|
||||
let tilt = ctx.params.f("tilt").to_radians();
|
||||
let dist = ctx.params.f("distanceToImage") * b.scale;
|
||||
let spec_on = ctx.params.b("specularHighlight");
|
||||
let wire = ctx.params.b("preview/drawPreviewWireframe");
|
||||
if swivel == 0.0 && tilt == 0.0 && dist == 0.0 && !spec_on && !wire {
|
||||
return Some(b);
|
||||
}
|
||||
let (lx, ly, lw, lh) = (b.offset[0], b.offset[1], ctx.layer_size[0] * b.scale, ctx.layer_size[1] * b.scale);
|
||||
let (cx, cy) = (lx + lw * 0.5, ly + lh * 0.5);
|
||||
let f = lw.max(lh).max(1.0) * 1.4;
|
||||
let (cs, ss) = (swivel.cos(), swivel.sin());
|
||||
let (ct, st) = (tilt.cos(), tilt.sin());
|
||||
let v3 = |v: [f64; 3]| [v[0] as f32, v[1] as f32, v[2] as f32, 0.0];
|
||||
let l = [-0.5f64, -0.6, -1.0];
|
||||
let k = 1.0 / (l[0] * l[0] + l[1] * l[1] + l[2] * l[2]).sqrt();
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [spec_on as u32, wire as u32, 0, 0];
|
||||
p.f[0] = [cx as f32, cy as f32, f as f32, dist as f32];
|
||||
p.f[1] = v3([cs, 0.0, -ss]);
|
||||
p.f[2] = v3([ss * st, ct, cs * st]);
|
||||
p.f[3] = v3([ss * ct, -st, cs * ct]);
|
||||
p.f[4] = v3(l.map(|v| v * k));
|
||||
p.f[5] = [(lw * 0.5) as f32, (lh * 0.5) as f32, 0.0, 0.0];
|
||||
run(e, "fp2_basic3d", &p, b, None, None)
|
||||
}
|
||||
+14
-6
@@ -22,14 +22,16 @@
|
||||
//! GPU effects ([`effectcraft_effects::GPU_EFFECTS`]) run as compute kernels with the CPU
|
||||
//! effect's exact steps (padding, box-blur radii, parameter conversions); chains of them are
|
||||
//! uploaded and read back once. Each family lives in its own module with its own WGSL file
|
||||
//! (`fx_color`, `fx_depth`, `fx_distort`, `fx_extra`, `fx_generate`, `fx_key`, `fx_light`,
|
||||
//! `fx_lut`, `fx_noise`, `fx_particles`, `fx_sim`, `fx_stylize`, `fx_text`, `fx_time`, `fx_tone`,
|
||||
//! `fx_transition`, `fx_vr`, `fx_warp`); settings a kernel cannot match fall back to the CPU
|
||||
//! (`catalog::gpu_supported`, or `None` from the family's `apply`).
|
||||
//! (`fx_color`, `fx_depth`, `fx_distort`, `fx_extra`, `fx_gen2`, `fx_generate`, `fx_key`,
|
||||
//! `fx_light`, `fx_lut`, `fx_noise`, `fx_particles`, `fx_pixel2`, `fx_sim`, `fx_stylize`,
|
||||
//! `fx_text`, `fx_time`, `fx_tone`, `fx_transition`, `fx_vr`, `fx_warp`); settings a kernel
|
||||
//! cannot match fall back to the CPU (`catalog::gpu_supported`, or `None` from the family's
|
||||
//! `apply`).
|
||||
//! The 3D Channel effects read the layer's aux channels as an extra texture (`fx_depth`), the
|
||||
//! colour-management effects interpret colour programs built next to the CPU effects
|
||||
//! (`fx_lut`), and simulations keep their state on the CPU and rasterise its per-frame plan here
|
||||
//! (`fx_sim`, `fx_particles`).
|
||||
//! (`fx_lut`), simulations keep their state on the CPU and rasterise its per-frame plan here
|
||||
//! (`fx_sim`, `fx_particles`), and so do the geometry generators (`fx_gen2`: bolts, strokes,
|
||||
//! glyphs, audio marks, waves).
|
||||
//!
|
||||
//! Working textures come from a pool (`context::Pool`): a frame allocates several full-frame
|
||||
//! RGBA f32 images per layer, and creating and zeroing those cost more than compositing a small
|
||||
@@ -59,12 +61,14 @@ mod fx_color;
|
||||
mod fx_depth;
|
||||
mod fx_distort;
|
||||
mod fx_extra;
|
||||
mod fx_gen2;
|
||||
mod fx_generate;
|
||||
mod fx_key;
|
||||
mod fx_light;
|
||||
mod fx_lut;
|
||||
mod fx_noise;
|
||||
mod fx_particles;
|
||||
mod fx_pixel2;
|
||||
mod fx_sim;
|
||||
mod fx_stylize;
|
||||
mod fx_text;
|
||||
@@ -335,6 +339,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;
|
||||
@@ -347,6 +353,8 @@ mod tests_fx_noise;
|
||||
#[cfg(test)]
|
||||
mod tests_fx_particles;
|
||||
#[cfg(test)]
|
||||
mod tests_fx_pixel2;
|
||||
#[cfg(test)]
|
||||
mod tests_fx_sim;
|
||||
#[cfg(test)]
|
||||
mod tests_fx_stylize;
|
||||
|
||||
@@ -0,0 +1,805 @@
|
||||
// GPU effects, generators of part C: see src/fx_gen2.rs. Every entry point is prefixed `fg2_`.
|
||||
// Geometry computed on the CPU (bolts, strokes, glyphs, audio marks, waves) arrives as item
|
||||
// tables in `data` — tile offsets, item indices, items (as fx_sim's tiled raster) — and is
|
||||
// rasterised here with the CPU's coverage functions; closed-form generators run per pixel.
|
||||
|
||||
fn fg2_inside(p: vec2<i32>) -> bool {
|
||||
let d = out_dims();
|
||||
return p.x < d.x && p.y < d.y;
|
||||
}
|
||||
|
||||
// util::unpremul (colour 0 below alpha 1e-6).
|
||||
fn fg2_unpremul(p: vec4<f32>) -> vec3<f32> {
|
||||
if (p.w > 1e-6) {
|
||||
return p.xyz / p.w;
|
||||
}
|
||||
return vec3<f32>(0.0);
|
||||
}
|
||||
|
||||
// util::smoothstep.
|
||||
fn fg2_smoothstep(e0: f32, e1: f32, x: f32) -> f32 {
|
||||
if (abs(e1 - e0) < 1e-9) {
|
||||
return select(1.0, 0.0, x < e0);
|
||||
}
|
||||
let t = clamp((x - e0) / (e1 - e0), 0.0, 1.0);
|
||||
return t * t * (3.0 - 2.0 * t);
|
||||
}
|
||||
|
||||
// Premultiplied "over": `s` on top of `d`.
|
||||
fn fg2_over(d: vec4<f32>, s: vec4<f32>) -> vec4<f32> {
|
||||
return s + d * (1.0 - s.w);
|
||||
}
|
||||
|
||||
// The tile's item range: (first index slot, end slot).
|
||||
fn fg2_tile(p: vec2<i32>, tiles_x: u32) -> vec2<u32> {
|
||||
let tile = u32(p.y / 16) * tiles_x + u32(p.x / 16);
|
||||
return vec2<u32>(u32(data[tile]), u32(data[tile + 1u]));
|
||||
}
|
||||
|
||||
// Whether pixel `p` lies in the integer bounds stored at the end of item `at` (stride `stride`).
|
||||
fn fg2_in_bounds(p: vec2<i32>, at: u32, stride: u32) -> bool {
|
||||
let bb = at + stride - 4u;
|
||||
return !(f32(p.x) < data[bb] || f32(p.y) < data[bb + 1u] || f32(p.x) > data[bb + 2u] || f32(p.y) > data[bb + 3u]);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- segments (generate3::raster_segs)
|
||||
// Items: (ax, ay, bx, by, radius, value, bounds). Up to four coverages per pixel (x, y, z, w),
|
||||
// variant k with radius max(r · f[k].x + f[k].y, f[k].z) and hardness f[k].w, max-combined;
|
||||
// bit k of u[1].x selects Advanced Lightning's linear core (clamp(r − d + 0.5, 0, 1)) instead.
|
||||
// u[0] = (items at, stride, tiles x, variants).
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fg2_segs(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fg2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let stride = P.u[0].y;
|
||||
let nv = P.u[0].w;
|
||||
let rg = fg2_tile(p, P.u[0].z);
|
||||
let px = f32(p.x) + 0.5;
|
||||
let py = f32(p.y) + 0.5;
|
||||
var o = vec4<f32>(0.0);
|
||||
for (var k = rg.x; k < rg.y; k++) {
|
||||
let at = P.u[0].x + u32(data[k]) * stride;
|
||||
if (!fg2_in_bounds(p, at, stride)) {
|
||||
continue;
|
||||
}
|
||||
let ax = data[at];
|
||||
let ay = data[at + 1u];
|
||||
let dx = data[at + 2u] - ax;
|
||||
let dy = data[at + 3u] - ay;
|
||||
let r = data[at + 4u];
|
||||
let v = data[at + 5u];
|
||||
let l2 = dx * dx + dy * dy;
|
||||
var t = 0.0;
|
||||
if (l2 > 1e-12) {
|
||||
t = clamp(((px - ax) * dx + (py - ay) * dy) / l2, 0.0, 1.0);
|
||||
}
|
||||
let ex = px - ax - dx * t;
|
||||
let ey = py - ay - dy * t;
|
||||
let d = sqrt(ex * ex + ey * ey);
|
||||
for (var j = 0u; j < nv; j++) {
|
||||
let vf = P.f[j];
|
||||
let rk = max(r * vf.x + vf.y, vf.z);
|
||||
var c = 0.0;
|
||||
if (((P.u[1].x >> j) & 1u) != 0u) {
|
||||
c = clamp(rk - d + 0.5, 0.0, 1.0) * v;
|
||||
} else {
|
||||
let inner = rk * clamp(vf.w, 0.0, 1.0) - 0.5;
|
||||
let outer = rk + 0.5;
|
||||
if (d >= outer) {
|
||||
continue;
|
||||
}
|
||||
if (d <= inner) {
|
||||
c = v;
|
||||
} else {
|
||||
c = (1.0 - fg2_smoothstep(inner, outer, d)) * v;
|
||||
}
|
||||
}
|
||||
if (c > o[j]) {
|
||||
o[j] = c;
|
||||
}
|
||||
}
|
||||
}
|
||||
textureStore(out, p, o);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- convex polygons (generate3::raster_convex)
|
||||
// Items: (vertex count, 16 vertex pairs, bounds); coverage = union, 4 × 4 samples per pixel.
|
||||
// u[0] = (items at, stride, tiles x).
|
||||
|
||||
fn fg2_in_poly(at: u32, x: f32, y: f32) -> bool {
|
||||
let n = u32(data[at]);
|
||||
var sgn = 0.0;
|
||||
for (var i = 0u; i < n; i++) {
|
||||
let j = (i + 1u) % n;
|
||||
let ax = data[at + 1u + 2u * i];
|
||||
let ay = data[at + 2u + 2u * i];
|
||||
let bx = data[at + 1u + 2u * j];
|
||||
let by = data[at + 2u + 2u * j];
|
||||
let c = (bx - ax) * (y - ay) - (by - ay) * (x - ax);
|
||||
if (abs(c) < 1e-12) {
|
||||
continue;
|
||||
}
|
||||
if (sgn == 0.0) {
|
||||
sgn = sign(c);
|
||||
} else if (sign(c) != sgn) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fg2_convex(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fg2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let stride = P.u[0].y;
|
||||
let rg = fg2_tile(p, P.u[0].z);
|
||||
var o = 0.0;
|
||||
for (var k = rg.x; k < rg.y; k++) {
|
||||
let at = P.u[0].x + u32(data[k]) * stride;
|
||||
if (!fg2_in_bounds(p, at, stride)) {
|
||||
continue;
|
||||
}
|
||||
var hit = 0;
|
||||
for (var sy = 0; sy < 4; sy++) {
|
||||
for (var sx = 0; sx < 4; sx++) {
|
||||
if (fg2_in_poly(at, f32(p.x) + (f32(sx) + 0.5) / 4.0, f32(p.y) + (f32(sy) + 0.5) / 4.0)) {
|
||||
hit += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
o = max(o, f32(hit) / 16.0);
|
||||
}
|
||||
textureStore(out, p, vec4<f32>(o, 0.0, 0.0, 0.0));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- painting
|
||||
// generate3::paint (style 0 on the layer, 1 on transparency, 2 reveal) and Vegas' Composite
|
||||
// Under (3). aux = coverage (x); u[0].x = style; f[0] = colour; f[1].x = opacity.
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fg2_paint(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fg2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let px = textureLoad(src, p, 0);
|
||||
let c = textureLoad(aux, p, 0).x;
|
||||
let col = P.f[0];
|
||||
var o: vec4<f32>;
|
||||
if (P.u[0].x == 3u) {
|
||||
let a = c * col.w;
|
||||
o = fg2_over(vec4<f32>(col.xyz * a, a), px);
|
||||
} else {
|
||||
let a = clamp(c * P.f[1].x * col.w, 0.0, 1.0);
|
||||
let s = vec4<f32>(col.xyz * a, a);
|
||||
switch P.u[0].x {
|
||||
case 1u: {
|
||||
o = s;
|
||||
}
|
||||
case 2u: {
|
||||
o = px * a;
|
||||
}
|
||||
default: {
|
||||
o = fg2_over(px, s);
|
||||
}
|
||||
}
|
||||
}
|
||||
textureStore(out, p, o);
|
||||
}
|
||||
|
||||
// generate3::draw_marks: aux = (core, halo); u[0].x = composite; f[0] = inside, f[1] = outside.
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fg2_marks(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fg2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let cv = textureLoad(aux, p, 0);
|
||||
let c = cv.x;
|
||||
let a = max(c, cv.y);
|
||||
var base = vec4<f32>(0.0);
|
||||
if (P.u[0].x != 0u) {
|
||||
base = textureLoad(src, p, 0);
|
||||
}
|
||||
if (a <= 0.0) {
|
||||
textureStore(out, p, base);
|
||||
return;
|
||||
}
|
||||
let t = c / a;
|
||||
let col = P.f[1].xyz + (P.f[0].xyz - P.f[1].xyz) * t;
|
||||
textureStore(out, p, fg2_over(base, vec4<f32>(col * a, a)));
|
||||
}
|
||||
|
||||
// (src.x, aux.x) into (x, y), src.y into z: a coverage next to its blurred copy.
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fg2_pair(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fg2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let s = textureLoad(src, p, 0);
|
||||
textureStore(out, p, vec4<f32>(s.x, textureLoad(aux, p, 0).x, s.y, 0.0));
|
||||
}
|
||||
|
||||
// textfx::lightning's composite: aux = (outer, glow, core); u[0].x = blending mode;
|
||||
// f[0] = outside colour, f[1] = inside colour.
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fg2_bolt(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fg2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let px = textureLoad(src, p, 0);
|
||||
let m = textureLoad(aux, p, 0);
|
||||
let o = max(m.x, m.y * 0.8);
|
||||
let c = m.z;
|
||||
let a = min(max(o, c), 1.0);
|
||||
if (a <= 0.0) {
|
||||
textureStore(out, p, px);
|
||||
return;
|
||||
}
|
||||
let t = c / a;
|
||||
let oc = P.f[0].xyz;
|
||||
let colr = oc + (P.f[1].xyz - oc) * t;
|
||||
var r: vec4<f32>;
|
||||
switch P.u[0].x {
|
||||
case 1u: {
|
||||
r = vec4<f32>(px.xyz + colr * a, min(px.w + a * (1.0 - px.w), 1.0));
|
||||
}
|
||||
case 2u: {
|
||||
let s = vec4<f32>(colr * a, a);
|
||||
r = px + s - px * s;
|
||||
}
|
||||
default: {
|
||||
r = fg2_over(px, vec4<f32>(colr * a, a));
|
||||
}
|
||||
}
|
||||
textureStore(out, p, r);
|
||||
}
|
||||
|
||||
// generate2::advanced_lightning's composite: aux = (core, blurred core); u[0] = (composite, glow);
|
||||
// f[0] = (core opacity, glow opacity); f[1] = core colour, f[2] = glow colour.
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fg2_adv_bolt(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fg2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let px = textureLoad(src, p, 0);
|
||||
let m = textureLoad(aux, p, 0);
|
||||
let cv = m.x * P.f[0].x;
|
||||
var g = 0.0;
|
||||
if (P.u[0].y != 0u) {
|
||||
g = min(m.y * 3.0, 1.0);
|
||||
}
|
||||
let gv = g * P.f[0].y;
|
||||
let l = P.f[1].xyz * cv + P.f[2].xyz * gv;
|
||||
let la = min(max(cv, gv), 1.0);
|
||||
if (P.u[0].x != 0u) {
|
||||
let k = clamp(la, 0.0, 1.0);
|
||||
textureStore(out, p, vec4<f32>(px.xyz + l, clamp(px.w + k * (1.0 - px.w), 0.0, 1.0)));
|
||||
} else {
|
||||
textureStore(out, p, vec4<f32>(l, la));
|
||||
}
|
||||
}
|
||||
|
||||
// textfx::text_px over the previous pass: aux = (fill, outer, inner) coverages.
|
||||
// u[0] = (display, keep the layer, stroked, inner ring); f[0] = fill, f[1] = stroke colour;
|
||||
// f[2].x = opacity.
|
||||
fn fg2_tint(c: vec4<f32>, a0: f32) -> vec4<f32> {
|
||||
let a = clamp(a0 * c.w, 0.0, 1.0);
|
||||
return vec4<f32>(c.xyz * a, a);
|
||||
}
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fg2_text(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fg2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let cov = textureLoad(aux, p, 0);
|
||||
let op = P.f[2].x;
|
||||
let f = cov.x * op;
|
||||
var ring = 0.0;
|
||||
if (P.u[0].z != 0u) {
|
||||
var inner = 0.0;
|
||||
if (P.u[0].w != 0u) {
|
||||
inner = cov.z;
|
||||
}
|
||||
ring = max(cov.y - inner, 0.0);
|
||||
}
|
||||
let s = ring * op;
|
||||
var o = vec4<f32>(0.0);
|
||||
if (P.u[0].y != 0u) {
|
||||
o = textureLoad(src, p, 0);
|
||||
}
|
||||
let fill = fg2_tint(P.f[0], f);
|
||||
let stroke = fg2_tint(P.f[1], s);
|
||||
switch P.u[0].x {
|
||||
case 1u: {
|
||||
o = fg2_over(o, stroke);
|
||||
}
|
||||
case 2u: {
|
||||
o = fg2_over(fg2_over(o, stroke), fill);
|
||||
}
|
||||
case 3u: {
|
||||
o = fg2_over(fg2_over(o, fill), stroke);
|
||||
}
|
||||
default: {
|
||||
o = fg2_over(o, fill);
|
||||
}
|
||||
}
|
||||
textureStore(out, p, o);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Radio Waves (generate2::radio_waves)
|
||||
// data: waves (stride 13: cx, cy, radius, half width, rotation, fade, colour rgba, opacity,
|
||||
// outline offset, outline vertices), then outline vertices (x, y relative to the centre).
|
||||
// aux = the contour's signed distance (x) when u[0].y. u[0] = (waves, contour, profile);
|
||||
// f[0] = (anti-aliasing, anchor x, anchor y).
|
||||
|
||||
fn fg2_rw_profile(kind: u32, u0: f32) -> f32 {
|
||||
let u = clamp(u0, -1.0, 1.0);
|
||||
switch kind {
|
||||
case 1u: {
|
||||
return (1.0 - u) * 0.5;
|
||||
}
|
||||
case 2u: {
|
||||
return (1.0 + u) * 0.5;
|
||||
}
|
||||
case 3u: {
|
||||
return 1.0 - abs(u);
|
||||
}
|
||||
case 4u: {
|
||||
return cos(u * 1.5707963267948966);
|
||||
}
|
||||
case 5u: {
|
||||
if (u < 0.0) {
|
||||
return 1.0 + u;
|
||||
}
|
||||
return 1.0;
|
||||
}
|
||||
case 6u: {
|
||||
if (u > 0.0) {
|
||||
return 1.0 - u;
|
||||
}
|
||||
return 1.0;
|
||||
}
|
||||
default: {
|
||||
return 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// generate2::outline_dist.
|
||||
fn fg2_outline_dist(off: u32, n: u32, dx: f32, dy: f32) -> f32 {
|
||||
let r = sqrt(dx * dx + dy * dy);
|
||||
if (r < 1e-9) {
|
||||
var m = 3.0e38;
|
||||
for (var k = 0u; k < n; k++) {
|
||||
let x = data[off + 2u * k];
|
||||
let y = data[off + 2u * k + 1u];
|
||||
m = min(m, sqrt(x * x + y * y));
|
||||
}
|
||||
return m;
|
||||
}
|
||||
let dir = vec2<f32>(dx / r, dy / r);
|
||||
var best = 3.0e38;
|
||||
for (var k = 0u; k < n; k++) {
|
||||
let j = (k + 1u) % n;
|
||||
let p0 = vec2<f32>(data[off + 2u * k], data[off + 2u * k + 1u]);
|
||||
let p1 = vec2<f32>(data[off + 2u * j], data[off + 2u * j + 1u]);
|
||||
let e = p1 - p0;
|
||||
let den = dir.x * e.y - dir.y * e.x;
|
||||
if (abs(den) < 1e-12) {
|
||||
continue;
|
||||
}
|
||||
let t = (p0.x * e.y - p0.y * e.x) / den;
|
||||
let q = vec2<f32>(dir.x * t - p0.x, dir.y * t - p0.y);
|
||||
let el2 = max(e.x * e.x + e.y * e.y, 1e-12);
|
||||
let s = (q.x * e.x + q.y * e.y) / el2;
|
||||
if (t <= 0.0 || s < -1e-5 || s > 1.0 + 1e-5) {
|
||||
continue;
|
||||
}
|
||||
best = min(best, abs(r - t) * abs(den) / sqrt(el2));
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
// util::point_in_poly (even-odd).
|
||||
fn fg2_point_in_poly(off: u32, n: u32, x: f32, y: f32) -> bool {
|
||||
if (n < 3u) {
|
||||
return false;
|
||||
}
|
||||
var inside = false;
|
||||
var j = n - 1u;
|
||||
for (var i = 0u; i < n; i++) {
|
||||
let ax = data[off + 2u * i];
|
||||
let ay = data[off + 2u * i + 1u];
|
||||
let bx = data[off + 2u * j];
|
||||
let by = data[off + 2u * j + 1u];
|
||||
if ((ay > y) != (by > y) && x < (bx - ax) * (y - ay) / (by - ay) + ax) {
|
||||
inside = !inside;
|
||||
}
|
||||
j = i;
|
||||
}
|
||||
return inside;
|
||||
}
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fg2_radio(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fg2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
var o = textureLoad(src, p, 0);
|
||||
let aa = P.f[0].x;
|
||||
for (var w = 0u; w < P.u[0].x; w++) {
|
||||
let at = w * 13u;
|
||||
let dx = f32(p.x) + 0.5 - data[at];
|
||||
let dy = f32(p.y) + 0.5 - data[at + 1u];
|
||||
let radius = data[at + 2u];
|
||||
let hw = data[at + 3u];
|
||||
let n = u32(data[at + 12u]);
|
||||
var d: f32;
|
||||
if (P.u[0].y != 0u) {
|
||||
let rot = data[at + 4u];
|
||||
let s = sin(-rot);
|
||||
let c = cos(-rot);
|
||||
let rx = dx * c - dy * s;
|
||||
let ry = dx * s + dy * c;
|
||||
d = sample_bilinear_clamped(aux, P.f[0].y + rx, P.f[0].z + ry).x - radius;
|
||||
} else if (n > 0u) {
|
||||
let off = u32(data[at + 11u]);
|
||||
let ud = fg2_outline_dist(off, n, dx, dy);
|
||||
d = select(ud, -ud, fg2_point_in_poly(off, n, dx, dy));
|
||||
} else {
|
||||
d = sqrt(dx * dx + dy * dy) - radius;
|
||||
}
|
||||
let ad = abs(d);
|
||||
let cov = clamp((hw - ad) / aa + 0.5, 0.0, 1.0);
|
||||
if (cov <= 0.0) {
|
||||
continue;
|
||||
}
|
||||
let u = clamp(d / hw, -1.0, 1.0);
|
||||
let col = vec4<f32>(data[at + 6u], data[at + 7u], data[at + 8u], data[at + 9u]);
|
||||
let a = cov * fg2_rw_profile(P.u[0].z, u) * data[at + 5u] * data[at + 10u] * col.w;
|
||||
o = fg2_over(o, vec4<f32>(col.xyz * a, a));
|
||||
}
|
||||
textureStore(out, p, o);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Beam (generate2::beam)
|
||||
// u[0] = (3D perspective, composite, length > 0); f[0] = (sx, sy, dx, dy); f[1] = (t0, t1,
|
||||
// start thickness, end thickness); f[2] = (softness, |d|², ...); f[3] = inside, f[4] = outside.
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fg2_beam(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fg2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let px = textureLoad(src, p, 0);
|
||||
let s = P.f[0].xy;
|
||||
let d = P.f[0].zw;
|
||||
let t0 = P.f[1].x;
|
||||
let t1 = P.f[1].y;
|
||||
let soft = P.f[2].x;
|
||||
let vx = f32(p.x) + 0.5 - s.x;
|
||||
let vy = f32(p.y) + 0.5 - s.y;
|
||||
let tc = clamp((vx * d.x + vy * d.y) / P.f[2].y, t0, t1);
|
||||
let ex = vx - d.x * tc;
|
||||
let ey = vy - d.y * tc;
|
||||
let dist = sqrt(ex * ex + ey * ey);
|
||||
var tk = tc;
|
||||
if (!(P.u[0].x != 0u || t1 - t0 < 1e-9)) {
|
||||
tk = (tc - t0) / (t1 - t0);
|
||||
}
|
||||
let hw = max((P.f[1].z + (P.f[1].w - P.f[1].z) * tk) * 0.5, 0.25);
|
||||
let tt = dist / hw;
|
||||
var a = 0.0;
|
||||
if (P.u[0].z == 0u) {
|
||||
a = 0.0;
|
||||
} else if (soft > 1e-3) {
|
||||
a = 1.0 - fg2_smoothstep(1.0 - soft, 1.0, tt);
|
||||
} else {
|
||||
a = clamp(hw - dist + 0.5, 0.0, 1.0);
|
||||
}
|
||||
let cc = P.f[3].xyz + (P.f[4].xyz - P.f[3].xyz) * fg2_smoothstep(0.0, 1.0, tt);
|
||||
let g = vec4<f32>(cc * a, a);
|
||||
if (P.u[0].y != 0u) {
|
||||
textureStore(out, p, fg2_over(px, g));
|
||||
} else {
|
||||
textureStore(out, p, g);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Lens Flare (generate2::lens_flare)
|
||||
// data: ghosts (t, r, colour rgb, k, ring); u[0] = ghosts; f[0] = (fx, fy, cx, cy);
|
||||
// f[1] = (diagonal, core k, rays, halo r); f[2] = (brightness, blend).
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fg2_flare(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fg2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let px = textureLoad(src, p, 0);
|
||||
let f = P.f[0].xy;
|
||||
let c = P.f[0].zw;
|
||||
let diag = P.f[1].x;
|
||||
let dx = f32(p.x) + 0.5 - f.x;
|
||||
let dy = f32(p.y) + 0.5 - f.y;
|
||||
let r = sqrt(dx * dx + dy * dy) / diag;
|
||||
let th = atan2(dy, dx);
|
||||
let ck = r / P.f[1].y;
|
||||
let core = 1.5 * exp(-(ck * ck)) + 0.25 * exp(-r / 0.12);
|
||||
let streak = powz(abs(cos(P.f[1].z * 0.5 * th)), 40.0) * exp(-r / 0.3) * 0.35;
|
||||
let hk = (r - P.f[1].w) / 0.012;
|
||||
let halo = exp(-(hk * hk)) * 0.12;
|
||||
let base = core + streak;
|
||||
var l = vec3<f32>(base, base * 0.95, base * 0.85);
|
||||
l += vec3<f32>(halo * 0.6, halo * 0.8, halo);
|
||||
for (var i = 0u; i < P.u[0].x; i++) {
|
||||
let at = i * 7u;
|
||||
let gt = data[at];
|
||||
let gr = data[at + 1u];
|
||||
let gx = f.x + (c.x - f.x) * gt;
|
||||
let gy = f.y + (c.y - f.y) * gt;
|
||||
let ex = f32(p.x) + 0.5 - gx;
|
||||
let ey = f32(p.y) + 0.5 - gy;
|
||||
let d = sqrt(ex * ex + ey * ey) / diag;
|
||||
var v: f32;
|
||||
if (data[at + 6u] != 0.0) {
|
||||
let q = (d - gr) / (gr * 0.12);
|
||||
v = exp(-(q * q));
|
||||
} else {
|
||||
v = fg2_smoothstep(gr, gr * 0.8, d);
|
||||
}
|
||||
l += vec3<f32>(data[at + 2u], data[at + 3u], data[at + 4u]) * data[at + 5u] * v;
|
||||
}
|
||||
l = l * P.f[2].x;
|
||||
let la = min(max(max(l.x, l.y), l.z), 1.0);
|
||||
let k = clamp(la, 0.0, 1.0);
|
||||
let o = vec4<f32>(px.xyz + l, clamp(px.w + k * (1.0 - px.w), 0.0, 1.0));
|
||||
textureStore(out, p, o + (px - o) * P.f[2].y);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Glue Gun (generate3::glue_gun)
|
||||
// data: beads (x, y, radius); u[0] = (beads, glue only); f[0] = (light x, y, z, strength);
|
||||
// f[1].x = strength weight (clamp(strength, 0, 1) max 0.3).
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fg2_glue(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fg2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let px = textureLoad(src, p, 0);
|
||||
let glue_only = P.u[0].y != 0u;
|
||||
let fx = f32(p.x) + 0.5;
|
||||
let fy = f32(p.y) + 0.5;
|
||||
var best_h = 0.0;
|
||||
var best_d = vec2<f32>(0.0);
|
||||
var best_r = 1.0;
|
||||
for (var i = 0u; i < P.u[0].x; i++) {
|
||||
let r = data[i * 3u + 2u];
|
||||
let dx = (fx - data[i * 3u]) / r;
|
||||
let dy = (fy - data[i * 3u + 1u]) / r;
|
||||
let d2 = dx * dx + dy * dy;
|
||||
if (d2 < 1.0) {
|
||||
let hgt = sqrt(1.0 - d2);
|
||||
if (hgt > best_h) {
|
||||
best_h = hgt;
|
||||
best_d = vec2<f32>(dx, dy);
|
||||
best_r = r;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (best_h <= 0.0) {
|
||||
textureStore(out, p, select(px, vec4<f32>(0.0), glue_only));
|
||||
return;
|
||||
}
|
||||
let nrm = vec3<f32>(best_d, best_h);
|
||||
let ndl = max(dot(nrm, P.f[0].xyz), 0.0);
|
||||
let spec = powz(ndl, 24.0);
|
||||
let strength = P.f[0].w;
|
||||
let s = sample_bilinear_clamped(src, fx - nrm.x * best_r * 0.5 * strength, fy - nrm.y * best_r * 0.5 * strength);
|
||||
let edge = fg2_smoothstep(0.0, 0.15, best_h);
|
||||
let shade = 0.65 + 0.35 * ndl;
|
||||
let g = vec4<f32>(s.xyz * shade + vec3<f32>(spec), max(s.w, 0.6));
|
||||
let gp = vec4<f32>(min(g.xyz, vec3<f32>(g.w + spec)), min(g.w, 1.0));
|
||||
var base = px;
|
||||
if (glue_only) {
|
||||
base = vec4<f32>(0.0);
|
||||
}
|
||||
var o = base + (gp - base) * (edge * P.f[1].x);
|
||||
o.w = clamp(o.w, 0.0, 1.0);
|
||||
textureStore(out, p, o);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Threads (generate3::threads)
|
||||
// u[0] = (overlaps); f[0] = (cx, cy, sin, cos); f[1] = (thread width, height, coverage,
|
||||
// shadowing); f[2].x = texture.
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fg2_threads(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fg2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let c = P.f[0].xy;
|
||||
let sn = P.f[0].z;
|
||||
let cs = P.f[0].w;
|
||||
let tw = P.f[1].x;
|
||||
let th = P.f[1].y;
|
||||
let coverage = P.f[1].z;
|
||||
let shadow = P.f[1].w;
|
||||
let tex_amt = P.f[2].x;
|
||||
let overlaps = i32(P.u[0].x);
|
||||
let dx = f32(p.x) + 0.5 - c.x;
|
||||
let dy = f32(p.y) + 0.5 - c.y;
|
||||
let u = dx * cs + dy * sn;
|
||||
let v = -dx * sn + dy * cs;
|
||||
let ci = i32(floor(u / tw));
|
||||
let cj = i32(floor(v / th));
|
||||
let fu = u / tw - f32(ci);
|
||||
let fv = v / th - f32(cj);
|
||||
let in_h = abs(fv - 0.5) < coverage * 0.5;
|
||||
let in_v = abs(fu - 0.5) < coverage * 0.5;
|
||||
let h_top = ((ci + cj) % (2 * overlaps) + 2 * overlaps) % (2 * overlaps) < overlaps;
|
||||
var horizontal: bool;
|
||||
if (in_h && in_v) {
|
||||
horizontal = h_top;
|
||||
} else if (in_h) {
|
||||
horizontal = true;
|
||||
} else if (in_v) {
|
||||
horizontal = false;
|
||||
} else {
|
||||
textureStore(out, p, vec4<f32>(0.0));
|
||||
return;
|
||||
}
|
||||
var su = u;
|
||||
var sv = (f32(cj) + 0.5) * th;
|
||||
if (!horizontal) {
|
||||
su = (f32(ci) + 0.5) * tw;
|
||||
sv = v;
|
||||
}
|
||||
let s = sample_bilinear_clamped(src, c.x + su * cs - sv * sn, c.y + su * sn + sv * cs);
|
||||
let half_cov = max(coverage * 0.5, 1e-6);
|
||||
var across = (fu - 0.5) / half_cov;
|
||||
var along = fv;
|
||||
if (horizontal) {
|
||||
across = (fv - 0.5) / half_cov;
|
||||
along = fu;
|
||||
}
|
||||
let profile = sqrt(max(1.0 - across * across, 0.0));
|
||||
let dive = 1.0 - abs(along - 0.5) * 2.0;
|
||||
let under = select(1.0, 0.0, horizontal == h_top);
|
||||
let shade = 1.0 - shadow * (1.0 - profile) * 0.8 - shadow * 0.25 * (1.0 - dive) * under;
|
||||
var fib = 1.0;
|
||||
if (tex_amt > 0.0) {
|
||||
let seed = bitcast<u32>(ci) ^ (bitcast<u32>(cj) << 8u);
|
||||
fib = 1.0 - tex_amt * 0.3 * hash_noise(u32(max(along * 64.0, 0.0)), u32(max(across * 8.0 + 8.0, 0.0)), seed);
|
||||
}
|
||||
let k = max(shade * fib, 0.0);
|
||||
textureStore(out, p, vec4<f32>(s.xyz * k, s.w));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Paint Bucket / Eyedropper Fill (generate3)
|
||||
|
||||
// Paint Bucket's fill: aux = coverage (x); u[0] = (mode); f[0] = colour; f[1].x = opacity.
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fg2_bucket(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fg2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let px = textureLoad(src, p, 0);
|
||||
let c = textureLoad(aux, p, 0).x;
|
||||
let col = P.f[0];
|
||||
let mode = P.u[0].x;
|
||||
let k = clamp(c * P.f[1].x * col.w, 0.0, 1.0);
|
||||
if (k <= 0.0) {
|
||||
textureStore(out, p, select(px, vec4<f32>(0.0), mode == 9u));
|
||||
return;
|
||||
}
|
||||
let dc = fg2_unpremul(px);
|
||||
let da = select(max(px.w, 0.0), px.w, px.w > 1e-6);
|
||||
let fc = col.xyz;
|
||||
var mixed = fc;
|
||||
switch mode {
|
||||
case 1u: {
|
||||
mixed = dc + fc;
|
||||
}
|
||||
case 2u: {
|
||||
mixed = dc * fc;
|
||||
}
|
||||
case 3u: {
|
||||
mixed = vec3<f32>(1.0) - (vec3<f32>(1.0) - dc) * (vec3<f32>(1.0) - fc);
|
||||
}
|
||||
case 4u, 5u, 6u, 7u: {
|
||||
let d = rgb_to_hsl(dc);
|
||||
let f = rgb_to_hsl(fc);
|
||||
switch mode {
|
||||
case 4u: {
|
||||
mixed = hsl_to_rgb(f.x, d.y, d.z);
|
||||
}
|
||||
case 5u: {
|
||||
mixed = hsl_to_rgb(d.x, f.y, d.z);
|
||||
}
|
||||
case 6u: {
|
||||
mixed = hsl_to_rgb(f.x, f.y, d.z);
|
||||
}
|
||||
default: {
|
||||
mixed = hsl_to_rgb(d.x, d.y, f.z);
|
||||
}
|
||||
}
|
||||
}
|
||||
default: {}
|
||||
}
|
||||
var o: vec4<f32>;
|
||||
switch mode {
|
||||
case 8u: {
|
||||
o = fg2_over(vec4<f32>(fc * k, k), px);
|
||||
}
|
||||
case 9u: {
|
||||
o = vec4<f32>(fc * k, k);
|
||||
}
|
||||
case 0u: {
|
||||
o = fg2_over(px, vec4<f32>(fc * k, k));
|
||||
}
|
||||
default: {
|
||||
if (da <= 1e-6) {
|
||||
o = px;
|
||||
} else {
|
||||
let c3 = dc + (mixed - dc) * k;
|
||||
o = vec4<f32>(c3 * da, da);
|
||||
}
|
||||
}
|
||||
}
|
||||
textureStore(out, p, o);
|
||||
}
|
||||
|
||||
// Paint Bucket's coverage from the region (x): u[0].x = 0 antialias (aux = the blurred region),
|
||||
// 1 = view threshold, 2 = stroke ring (aux = the eroded region, src = the dilated one).
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fg2_bucket_cov(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fg2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let a = textureLoad(src, p, 0).x;
|
||||
let b = textureLoad(aux, p, 0).x;
|
||||
switch P.u[0].x {
|
||||
case 0u: {
|
||||
// src = the region, aux = its blur.
|
||||
textureStore(out, p, vec4<f32>(select(b, max(b, 0.5), a > 0.0), 0.0, 0.0, 0.0));
|
||||
}
|
||||
case 1u: {
|
||||
textureStore(out, p, vec4<f32>(a, a, a, 1.0));
|
||||
}
|
||||
default: {
|
||||
textureStore(out, p, vec4<f32>(max(a - b, 0.0), 0.0, 0.0, 0.0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Eyedropper Fill: u[0].x = keep alpha; f[0] = fill (straight, alpha); f[1].x = blend.
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fg2_eyedropper(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fg2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let px = textureLoad(src, p, 0);
|
||||
let a = select(P.f[0].w, px.w, P.u[0].x != 0u);
|
||||
let f = vec4<f32>(P.f[0].xyz * a, a);
|
||||
textureStore(out, p, f + (px - f) * P.f[1].x);
|
||||
}
|
||||
@@ -0,0 +1,947 @@
|
||||
// GPU effects, pixel ports (part C): see src/fx_pixel2.rs. Every entry point is prefixed
|
||||
// `fp2_` and mirrors the CPU effect operation for operation; positions are pixel centres
|
||||
// relative to the effect's centre or anchor so f32 keeps its precision.
|
||||
|
||||
fn fp2_inside(p: vec2<i32>) -> bool {
|
||||
let d = out_dims();
|
||||
return p.x < d.x && p.y < d.y;
|
||||
}
|
||||
|
||||
// util::unpremul (colour 0 below alpha 1e-6).
|
||||
fn fp2_unpremul(p: vec4<f32>) -> vec3<f32> {
|
||||
if (p.w > 1e-6) {
|
||||
return p.xyz / p.w;
|
||||
}
|
||||
return vec3<f32>(0.0);
|
||||
}
|
||||
|
||||
// util::smoothstep.
|
||||
fn fp2_smoothstep(e0: f32, e1: f32, x: f32) -> f32 {
|
||||
if (abs(e1 - e0) < 1e-9) {
|
||||
return select(1.0, 0.0, x < e0);
|
||||
}
|
||||
let t = clamp((x - e0) / (e1 - e0), 0.0, 1.0);
|
||||
return t * t * (3.0 - 2.0 * t);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Cross Blur (blur2::cross_blur)
|
||||
// src = horizontal blur, aux = vertical blur; u[0].x = transfer mode.
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fp2_cross(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fp2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let a = textureLoad(src, p, 0);
|
||||
let v = textureLoad(aux, p, 0);
|
||||
var o: vec4<f32>;
|
||||
switch P.u[0].x {
|
||||
case 1u: {
|
||||
o = a + v;
|
||||
}
|
||||
case 2u: {
|
||||
o = a + v - a * v;
|
||||
}
|
||||
case 3u: {
|
||||
o = max(a, v);
|
||||
}
|
||||
default: {
|
||||
o = (a + v) * 0.5;
|
||||
}
|
||||
}
|
||||
o.w = clamp(o.w, 0.0, 1.0);
|
||||
textureStore(out, p, o);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Radial Blur (blur3::cc_radial_blur)
|
||||
// u[0].x = type; f[0] = (cx, cy, zoom, arc); f[1].x = quality.
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fp2_radial(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fp2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let kind = P.u[0].x;
|
||||
let c = P.f[0].xy;
|
||||
let zoom = P.f[0].z;
|
||||
let arc = P.f[0].w;
|
||||
let dx = f32(p.x) + 0.5 - c.x;
|
||||
let dy = f32(p.y) + 0.5 - c.y;
|
||||
let r = sqrt(dx * dx + dy * dy);
|
||||
var span = r * abs(zoom);
|
||||
if (kind >= 3u) {
|
||||
span = r * abs(arc);
|
||||
}
|
||||
let n = u32(clamp(ceil(span * P.f[1].x / 50.0), 1.0, 256.0));
|
||||
if (n <= 1u) {
|
||||
textureStore(out, p, textureLoad(src, p, 0));
|
||||
return;
|
||||
}
|
||||
var acc = vec4<f32>(0.0);
|
||||
var wsum = 0.0;
|
||||
for (var i = 0u; i < n; i++) {
|
||||
let t = f32(i) / f32(n - 1u);
|
||||
var s = vec2<f32>(0.0);
|
||||
var w = 1.0;
|
||||
switch kind {
|
||||
case 0u: {
|
||||
let k = 1.0 - zoom * t;
|
||||
s = vec2<f32>(c.x + dx * k, c.y + dy * k);
|
||||
}
|
||||
case 1u: {
|
||||
let k = 1.0 - zoom * t;
|
||||
s = vec2<f32>(c.x + dx * k, c.y + dy * k);
|
||||
w = (1.0 - t) + 0.05;
|
||||
}
|
||||
case 2u: {
|
||||
let k = 1.0 + zoom * (t - 0.5);
|
||||
s = vec2<f32>(c.x + dx * k, c.y + dy * k);
|
||||
}
|
||||
default: {
|
||||
let a = arc * (t - 0.5);
|
||||
let sn = sin(a);
|
||||
let co = cos(a);
|
||||
if (kind == 4u) {
|
||||
w = (1.0 - abs(2.0 * t - 1.0)) + 0.05;
|
||||
}
|
||||
s = vec2<f32>(c.x + dx * co - dy * sn, c.y + dx * sn + dy * co);
|
||||
}
|
||||
}
|
||||
acc += sample_bilinear(src, s.x, s.y) * w;
|
||||
wsum += w;
|
||||
}
|
||||
textureStore(out, p, acc / wsum);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- util::gauss_plane
|
||||
// One box pass (edges repeated) summing each window directly: the CPU's planes accumulate
|
||||
// in f64, so a running f32 sum would lose the small values (normalised blurs divide them).
|
||||
// u[0] = (radius, vertical).
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fp2_box(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fp2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let r = i32(P.u[0].x);
|
||||
var acc = vec4<f32>(0.0);
|
||||
for (var i = -r; i <= r; i++) {
|
||||
if (P.u[0].y != 0u) {
|
||||
acc += tex_get_clamped(src, p.x, p.y + i);
|
||||
} else {
|
||||
acc += tex_get_clamped(src, p.x + i, p.y);
|
||||
}
|
||||
}
|
||||
textureStore(out, p, acc * (1.0 / f32(2 * r + 1)));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- planes
|
||||
// One value per pixel into x. u[0] = (kind, property): kind 0 = luminance of the premultiplied
|
||||
// pixel (blur2::plum); 1 = CC Glass / Plastic height (stylize3::height_field: the property of
|
||||
// the straight colour times alpha, alpha itself); 2 = CC Mr. Smoothie's flow property (straight);
|
||||
// 3 = x clamped to [f[0].x, f[0].y] (CC Plastic's cut).
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fp2_plane(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fp2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let px = textureLoad(src, p, 0);
|
||||
let prop = P.u[0].y;
|
||||
var v = 0.0;
|
||||
if (P.u[0].x == 0u) {
|
||||
v = luminance(px.xyz);
|
||||
} else if (P.u[0].x == 3u) {
|
||||
v = clamp(px.x, P.f[0].x, P.f[0].y);
|
||||
} else {
|
||||
let c = fp2_unpremul(px);
|
||||
let a = px.w;
|
||||
switch prop {
|
||||
case 0u: {
|
||||
v = c.x;
|
||||
}
|
||||
case 1u: {
|
||||
v = c.y;
|
||||
}
|
||||
case 2u: {
|
||||
v = c.z;
|
||||
}
|
||||
case 3u: {
|
||||
v = a;
|
||||
}
|
||||
case 4u: {
|
||||
v = luminance(c);
|
||||
}
|
||||
case 5u: {
|
||||
v = rgb_to_hsl(c).z;
|
||||
}
|
||||
case 6u: {
|
||||
v = rgb_to_hsl(c).x;
|
||||
}
|
||||
default: {
|
||||
v = rgb_to_hsl(c).y;
|
||||
}
|
||||
}
|
||||
if (P.u[0].x == 1u && prop != 3u) {
|
||||
v = v * a;
|
||||
}
|
||||
}
|
||||
textureStore(out, p, vec4<f32>(v, 0.0, 0.0, 0.0));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Vector Blur (blur2::vector_blur)
|
||||
// aux = softened luminance map (x). u[0].x = type; f[0] = (amount, angle offset).
|
||||
|
||||
fn fp2_map(x: i32, y: i32) -> f32 {
|
||||
return tex_get_clamped(aux, x, y).x;
|
||||
}
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fp2_vector(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fp2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let d = vec2<i32>(textureDimensions(aux));
|
||||
let x = p.x;
|
||||
let y = p.y;
|
||||
let gx = (fp2_map(min(x + 1, d.x - 1), y) - fp2_map(max(x - 1, 0), y)) * 0.5;
|
||||
let gy = (fp2_map(x, min(y + 1, d.y - 1)) - fp2_map(x, max(y - 1, 0))) * 0.5;
|
||||
let mag = sqrt(gx * gx + gy * gy);
|
||||
let m = fp2_map(x, y);
|
||||
let kind = P.u[0].x;
|
||||
let amount = P.f[0].x;
|
||||
let off = P.f[0].y;
|
||||
var ang = 0.0;
|
||||
var len = amount;
|
||||
if (kind <= 2u) {
|
||||
if (mag < 1e-6) {
|
||||
textureStore(out, p, textureLoad(src, p, 0));
|
||||
return;
|
||||
}
|
||||
let g = atan2(gy, gx);
|
||||
var base = g + 1.5707963267948966;
|
||||
if (kind == 2u) {
|
||||
base = g;
|
||||
}
|
||||
if (kind != 1u) {
|
||||
len = amount * min(mag * 10.0, 1.0);
|
||||
}
|
||||
ang = base + off;
|
||||
} else {
|
||||
ang = m * 6.283185307179586 + off;
|
||||
if (kind != 3u) {
|
||||
len = amount * m;
|
||||
}
|
||||
}
|
||||
if (len < 0.5) {
|
||||
textureStore(out, p, textureLoad(src, p, 0));
|
||||
return;
|
||||
}
|
||||
let dx = cos(ang) * len;
|
||||
let dy = sin(ang) * len;
|
||||
let n = u32(clamp(ceil(len * 2.0), 2.0, 48.0));
|
||||
var acc = vec4<f32>(0.0);
|
||||
for (var i = 0u; i < n; i++) {
|
||||
let t = f32(i) / f32(n - 1u) - 0.5;
|
||||
acc += sample_bilinear_clamped(src, f32(x) + 0.5 + dx * t, f32(y) + 0.5 + dy * t) / f32(n);
|
||||
}
|
||||
textureStore(out, p, acc);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- per-pixel colour effects
|
||||
// u[0].x = effect: 0 Cineon Converter, 1 HDR Compander, 2 HDR Highlight Compression,
|
||||
// 3 CC Overbrights, 4 Color Link (alpha), 5 Color Link (blend), 6 CC Composite.
|
||||
|
||||
// utility::Cineon::log_to_lin; c = (offset, knee, white, gamma), r = (internal black, white).
|
||||
fn fp2_log_to_lin(v: f32, c: vec4<f32>, r: vec2<f32>) -> f32 {
|
||||
let code = v * 1023.0;
|
||||
var lin = (pow(10.0, (code - c.z) * 0.002 / 0.6) - c.x) / max(1.0 - c.x, 1e-6);
|
||||
lin = powz(max(lin, 0.0), 1.7 / max(c.w, 0.01));
|
||||
let k = c.y;
|
||||
if (k < 1.0 && lin > k) {
|
||||
let w = 1.0 - k;
|
||||
lin = k + w * (1.0 - exp(-(lin - k) / w));
|
||||
}
|
||||
return r.x + (r.y - r.x) * lin;
|
||||
}
|
||||
|
||||
// utility::Cineon::lin_to_log.
|
||||
fn fp2_lin_to_log(v: f32, c: vec4<f32>, r: vec2<f32>) -> f32 {
|
||||
let d = r.y - r.x;
|
||||
var lin = (v - r.x) / select(d, 1e-6, abs(d) < 1e-6);
|
||||
let k = c.y;
|
||||
if (k < 1.0 && lin > k) {
|
||||
let w = 1.0 - k;
|
||||
lin = k - w * log(max(1.0 - (lin - k) / w, 1e-6));
|
||||
}
|
||||
lin = powz(max(lin, 0.0), max(c.w, 0.01) / 1.7);
|
||||
let code = c.z + log(max(lin * (1.0 - c.x) + c.x, 1e-9)) * 0.4342944819032518 * 0.6 / 0.002;
|
||||
return code / 1023.0;
|
||||
}
|
||||
|
||||
// utility2::spow.
|
||||
fn fp2_spow(v: f32, e: f32) -> f32 {
|
||||
return sign(v) * powz(abs(v), e);
|
||||
}
|
||||
|
||||
fn fp2_cineon(v: f32) -> f32 {
|
||||
let c = P.f[0];
|
||||
let r = P.f[1].xy;
|
||||
switch P.u[0].y {
|
||||
case 0u: {
|
||||
return fp2_lin_to_log(v, c, r);
|
||||
}
|
||||
case 1u: {
|
||||
return fp2_log_to_lin(v, c, r);
|
||||
}
|
||||
default: {
|
||||
return fp2_lin_to_log(fp2_log_to_lin(v, c, r), P.f[2], vec2<f32>(0.0, 1.0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fp2_point(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fp2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let px = textureLoad(src, p, 0);
|
||||
var o = px;
|
||||
switch P.u[0].x {
|
||||
// f[0], f[1], f[2]: see fp2_cineon; u[0].y = conversion type.
|
||||
case 0u: {
|
||||
if (px.w > 0.0) {
|
||||
let c = px.xyz / px.w;
|
||||
o = vec4<f32>(vec3<f32>(fp2_cineon(c.x), fp2_cineon(c.y), fp2_cineon(c.z)) * px.w, px.w);
|
||||
}
|
||||
}
|
||||
// u[0].y = expand; f[0] = (gain, gamma).
|
||||
case 1u: {
|
||||
if (px.w > 0.0) {
|
||||
let c = fp2_unpremul(px);
|
||||
let g = P.f[0].x;
|
||||
let e = P.f[0].y;
|
||||
var r = vec3<f32>(0.0);
|
||||
if (P.u[0].y != 0u) {
|
||||
r = vec3<f32>(fp2_spow(c.x, e), fp2_spow(c.y, e), fp2_spow(c.z, e)) * g;
|
||||
} else {
|
||||
r = vec3<f32>(fp2_spow(c.x / g, 1.0 / e), fp2_spow(c.y / g, 1.0 / e), fp2_spow(c.z / g, 1.0 / e));
|
||||
}
|
||||
o = vec4<f32>(r * px.w, px.w);
|
||||
}
|
||||
}
|
||||
// f[0].x = knee.
|
||||
case 2u: {
|
||||
if (px.w > 0.0) {
|
||||
let c = px.xyz / px.w;
|
||||
let m = max(max(c.x, c.y), c.z);
|
||||
let knee = P.f[0].x;
|
||||
if (m > knee) {
|
||||
let w = 1.0 - knee;
|
||||
let k = (knee + w * (m - knee) / ((m - knee) + w)) / m;
|
||||
o = vec4<f32>(c * k * px.w, px.w);
|
||||
}
|
||||
}
|
||||
}
|
||||
// u[0].y = channel; f[0] = highlight colour.
|
||||
case 3u: {
|
||||
if (px.w > 0.0) {
|
||||
let c = fp2_unpremul(px);
|
||||
var v = max(max(c.x, c.y), c.z);
|
||||
switch P.u[0].y {
|
||||
case 1u: {
|
||||
v = c.x;
|
||||
}
|
||||
case 2u: {
|
||||
v = c.y;
|
||||
}
|
||||
case 3u: {
|
||||
v = c.z;
|
||||
}
|
||||
case 4u: {
|
||||
v = px.w;
|
||||
}
|
||||
case 5u: {
|
||||
v = luminance(c);
|
||||
}
|
||||
default: {}
|
||||
}
|
||||
var r = vec3<f32>(v * 0.35);
|
||||
if (v > 1.0) {
|
||||
r = P.f[0].xyz;
|
||||
} else if (v < 0.0) {
|
||||
r = vec3<f32>(1.0) - P.f[0].xyz;
|
||||
}
|
||||
o = vec4<f32>(r * px.w, px.w);
|
||||
}
|
||||
}
|
||||
// f[0] = (sampled alpha, opacity); u[0].y = stencil.
|
||||
case 4u: {
|
||||
let c = fp2_unpremul(px);
|
||||
let a0 = select(max(px.w, 0.0), px.w, px.w > 1e-6);
|
||||
var na = a0 + (P.f[0].x - a0) * P.f[0].y;
|
||||
if (P.u[0].y != 0u) {
|
||||
na = min(na, a0);
|
||||
}
|
||||
let a = clamp(na, 0.0, 1.0);
|
||||
o = vec4<f32>(c * a, a);
|
||||
}
|
||||
// f[0] = premultiplied sampled colour; u[0] = (stencil, mode).
|
||||
case 5u: {
|
||||
o = blend_pixel(P.u[0].z, px, P.f[0], 0.5);
|
||||
o.w = clamp(o.w, 0.0, 1.0);
|
||||
if (P.u[0].y != 0u) {
|
||||
o = vec4<f32>(fp2_unpremul(o) * px.w, px.w);
|
||||
}
|
||||
}
|
||||
// u[0] = (_, composite original, mode, rgb only); f[0].x = opacity.
|
||||
default: {
|
||||
let orig = px * P.f[0].x;
|
||||
if (P.u[0].y == 1u) {
|
||||
o = blend_pixel(P.u[0].z, orig, px, 0.5);
|
||||
} else {
|
||||
o = blend_pixel(P.u[0].z, px, orig, 0.5);
|
||||
}
|
||||
o.w = clamp(o.w, 0.0, 1.0);
|
||||
if (P.u[0].w != 0u) {
|
||||
o = vec4<f32>(fp2_unpremul(o) * px.w, px.w);
|
||||
}
|
||||
}
|
||||
}
|
||||
textureStore(out, p, o);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Block Load (stylize3::block_load)
|
||||
|
||||
// Mean colour of every s×s block: one invocation per block (the CPU's summation order).
|
||||
// u[0].x = s.
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fp2_block_means(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fp2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let s = i32(P.u[0].x);
|
||||
let d = vec2<i32>(textureDimensions(src));
|
||||
var acc = vec4<f32>(0.0);
|
||||
var n = 0.0;
|
||||
for (var y = p.y * s; y < min((p.y + 1) * s, d.y); y++) {
|
||||
for (var x = p.x * s; x < min((p.x + 1) * s, d.x); x++) {
|
||||
acc += textureLoad(src, vec2<i32>(x, y), 0);
|
||||
n += 1.0;
|
||||
}
|
||||
}
|
||||
if (n > 0.0) {
|
||||
acc = acc / n;
|
||||
}
|
||||
textureStore(out, p, acc);
|
||||
}
|
||||
|
||||
// stylize3::block_at on grid `t` (block size `s`).
|
||||
fn fp2_block_at(t: texture_2d<f32>, s: u32, x: i32, y: i32, bilinear: bool) -> vec4<f32> {
|
||||
let g = vec2<i32>(textureDimensions(t));
|
||||
if (!bilinear || s == 1u) {
|
||||
return textureLoad(t, vec2<i32>(min(x / i32(s), g.x - 1), min(y / i32(s), g.y - 1)), 0);
|
||||
}
|
||||
let fx = (f32(x) + 0.5) / f32(s) - 0.5;
|
||||
let fy = (f32(y) + 0.5) / f32(s) - 0.5;
|
||||
let x0 = floor(fx);
|
||||
let y0 = floor(fy);
|
||||
let tx = fx - x0;
|
||||
let ty = fy - y0;
|
||||
let xa = clamp(i32(x0), 0, g.x - 1);
|
||||
let xb = clamp(i32(x0 + 1.0), 0, g.x - 1);
|
||||
let ya = clamp(i32(y0), 0, g.y - 1);
|
||||
let yb = clamp(i32(y0 + 1.0), 0, g.y - 1);
|
||||
let a = textureLoad(t, vec2<i32>(xa, ya), 0);
|
||||
let b = textureLoad(t, vec2<i32>(xb, ya), 0);
|
||||
let c = textureLoad(t, vec2<i32>(xa, yb), 0);
|
||||
let e = textureLoad(t, vec2<i32>(xb, yb), 0);
|
||||
let top = a + (b - a) * tx;
|
||||
let bot = c + (e - c) * tx;
|
||||
return top + (bot - top) * ty;
|
||||
}
|
||||
|
||||
// src = the new level's grid, aux = the previous level's grid (or the new one).
|
||||
// u[0] = (new block size, old block size, flags: 1 scanlines, 2 smoothing, 4 bilinear,
|
||||
// 8 previous level exists, 16 start cleared); f[0] = (frac, rows of the new level).
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fp2_block_load(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fp2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let s_new = P.u[0].x;
|
||||
let flags = P.u[0].z;
|
||||
let scan = (flags & 1u) != 0u;
|
||||
let smoothing = (flags & 2u) != 0u;
|
||||
let bilinear = (flags & 4u) != 0u;
|
||||
let frac = P.f[0].x;
|
||||
let soft = select(0.0, 1.5, smoothing);
|
||||
var t = 0.0;
|
||||
if (scan) {
|
||||
let front = frac * (P.f[0].y + soft) - soft * 0.5;
|
||||
let r = f32(p.y / i32(s_new)) + 0.5;
|
||||
if (soft > 0.0) {
|
||||
t = fp2_smoothstep(r - soft * 0.5, r + soft * 0.5, front);
|
||||
} else if (r < front) {
|
||||
t = 1.0;
|
||||
}
|
||||
} else if (smoothing) {
|
||||
t = frac;
|
||||
}
|
||||
var n = vec4<f32>(0.0);
|
||||
if (t > 0.0) {
|
||||
n = fp2_block_at(src, s_new, p.x, p.y, bilinear);
|
||||
}
|
||||
var prev = vec4<f32>(0.0);
|
||||
if ((flags & 8u) != 0u) {
|
||||
prev = fp2_block_at(aux, P.u[0].y, p.x, p.y, bilinear);
|
||||
} else if ((flags & 16u) == 0u) {
|
||||
prev = fp2_block_at(src, s_new, p.x, p.y, bilinear);
|
||||
}
|
||||
var o = prev;
|
||||
if (t >= 1.0) {
|
||||
o = n;
|
||||
} else if (t > 0.0) {
|
||||
o = prev + (n - prev) * t;
|
||||
}
|
||||
textureStore(out, p, o);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Burn Film (stylize3::burn_film)
|
||||
// u[0].x = seed + 17; f[0] = (cx, cy, diagonal, feature size); f[1] = (threshold, band).
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fp2_burn(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fp2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let px = textureLoad(src, p, 0);
|
||||
let fx = f32(p.x) + 0.5;
|
||||
let fy = f32(p.y) + 0.5;
|
||||
let dx = fx - P.f[0].x;
|
||||
let dy = fy - P.f[0].y;
|
||||
let d = sqrt(dx * dx + dy * dy) / P.f[0].z;
|
||||
let n = fxs_fbm(fx / P.f[0].w, fy / P.f[0].w, 0.0, P.u[0].x, 4.0);
|
||||
let f = n * 0.65 + (1.0 - min(d, 1.0)) * 0.35;
|
||||
let th = P.f[1].x;
|
||||
let band = P.f[1].y;
|
||||
var o = px;
|
||||
if (f > th + band) {
|
||||
o = vec4<f32>(0.0);
|
||||
} else if (f > th - band * 2.0) {
|
||||
let c = fp2_unpremul(px);
|
||||
let k = fp2_smoothstep(th - band * 2.0, th, f);
|
||||
let hot = fp2_smoothstep(th, th + band, f);
|
||||
let chr = 1.0 - k * 0.85;
|
||||
var r = c * chr;
|
||||
r = vec3<f32>(r.x + (1.0 - r.x) * hot * 0.9, r.y + (0.45 - r.y) * hot * 0.9, r.z * (1.0 - hot));
|
||||
let na = px.w * (1.0 - hot * 0.6);
|
||||
o = vec4<f32>(r * na, na);
|
||||
}
|
||||
textureStore(out, p, o);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Glass / CC Plastic (stylize3)
|
||||
// aux = height field (x). u[0] = (glass, point light); f[0] = (height scale k, displacement,
|
||||
// height != 0, light height); f[1] = (intensity, ambient, diffuse, specular);
|
||||
// f[2] = (shininess, metal, light x, light y); f[3] = light colour; f[4] = distant direction.
|
||||
|
||||
fn fp2_h(x: i32, y: i32) -> f32 {
|
||||
return tex_get_clamped(aux, x, y).x;
|
||||
}
|
||||
|
||||
// stylize3::BumpLight::shade.
|
||||
fn fp2_shade(c: vec3<f32>, n: vec3<f32>, l: vec3<f32>) -> vec3<f32> {
|
||||
let intensity = P.f[1].x;
|
||||
let ambient = P.f[1].y;
|
||||
let diffuse = P.f[1].z;
|
||||
let specular = P.f[1].w;
|
||||
let ndl = max(dot(n, l), 0.0);
|
||||
let flat_z = max(l.z, 0.0);
|
||||
let base = ambient + diffuse * flat_z;
|
||||
var lit = 1.0;
|
||||
if (base > 1e-4) {
|
||||
lit = (ambient + diffuse * ndl) / base;
|
||||
}
|
||||
lit = 1.0 + (lit - 1.0) * intensity;
|
||||
let hv = vec3<f32>(l.x, l.y, l.z + 1.0);
|
||||
let hl = max(sqrt(hv.x * hv.x + hv.y * hv.y + hv.z * hv.z), 1e-6);
|
||||
let ndh = max((n.x * hv.x + n.y * hv.y + n.z * hv.z) / hl, 0.0);
|
||||
let spec = specular * intensity * powz(ndh, P.f[2].x);
|
||||
let lc = P.f[3].xyz;
|
||||
let metal = P.f[2].y;
|
||||
let hl_col = lc * (1.0 - metal) + c * lc * metal;
|
||||
return c * lit * (vec3<f32>(1.0) + (lc - vec3<f32>(1.0)) * min(intensity, 1.0)) + spec * hl_col;
|
||||
}
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fp2_bump(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fp2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let px = textureLoad(src, p, 0);
|
||||
let glass = P.u[0].x != 0u;
|
||||
if (px.w <= 0.0) {
|
||||
textureStore(out, p, select(px, vec4<f32>(0.0), glass));
|
||||
return;
|
||||
}
|
||||
let k = P.f[0].x;
|
||||
let gx = (fp2_h(p.x + 1, p.y) - fp2_h(p.x - 1, p.y)) * 0.5 * k;
|
||||
let gy = (fp2_h(p.x, p.y + 1) - fp2_h(p.x, p.y - 1)) * 0.5 * k;
|
||||
let ln = sqrt(gx * gx + gy * gy + 1.0);
|
||||
let n = vec3<f32>(-gx / ln, -gy / ln, 1.0 / ln);
|
||||
let fx = f32(p.x) + 0.5;
|
||||
let fy = f32(p.y) + 0.5;
|
||||
var l = P.f[4].xyz;
|
||||
if (P.u[0].y != 0u) {
|
||||
let v = vec3<f32>(P.f[2].z - fx, P.f[2].w - fy, max(P.f[0].w, 1.0));
|
||||
let vl = max(sqrt(v.x * v.x + v.y * v.y + v.z * v.z), 1e-6);
|
||||
l = v / vl;
|
||||
}
|
||||
var c = fp2_unpremul(px);
|
||||
if (glass) {
|
||||
let disp = P.f[0].y;
|
||||
if (disp != 0.0 && P.f[0].z != 0.0) {
|
||||
let s = sample_bilinear_clamped(src, fx + n.x * disp, fy + n.y * disp);
|
||||
if (s.w > 0.0) {
|
||||
c = fp2_unpremul(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
let o = fp2_shade(c, n, l);
|
||||
textureStore(out, p, vec4<f32>(o * px.w, px.w));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC HexTile (stylize3::hextile)
|
||||
// u[0].x = render mode; f[0] = (cx, cy, radius, smearing); f[1] = (sin, cos).
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fp2_hextile(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fp2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let cx = P.f[0].x;
|
||||
let cy = P.f[0].y;
|
||||
let r = P.f[0].z;
|
||||
let smear = P.f[0].w;
|
||||
let sn = P.f[1].x;
|
||||
let cs = P.f[1].y;
|
||||
let mode = P.u[0].x;
|
||||
let dx = f32(p.x) + 0.5 - cx;
|
||||
let dy = f32(p.y) + 0.5 - cy;
|
||||
let u = dx * cs + dy * sn;
|
||||
let v = -dx * sn + dy * cs;
|
||||
// stylize3::hex_cell (cube rounding).
|
||||
let q = (0.5773502691896257 * u - v / 3.0) / r;
|
||||
let rr = (2.0 / 3.0 * v) / r;
|
||||
let ccy = -q - rr;
|
||||
var rx = round_away(q);
|
||||
let ry = round_away(ccy);
|
||||
var rz = round_away(rr);
|
||||
let ddx = abs(rx - q);
|
||||
let ddy = abs(ry - ccy);
|
||||
let ddz = abs(rz - rr);
|
||||
if (ddx > ddy && ddx > ddz) {
|
||||
rx = -ry - rz;
|
||||
} else if (ddy <= ddz) {
|
||||
rz = -rx - ry;
|
||||
}
|
||||
let hx = r * 1.7320508075688772 * (rx + rz / 2.0);
|
||||
let hy = r * 1.5 * rz;
|
||||
var lu = u - hx;
|
||||
var lv = v - hy;
|
||||
let odd = ((i32(rx) + i32(rz)) % 2 + 2) % 2 == 1;
|
||||
if ((mode == 1u || mode == 2u) && odd) {
|
||||
lu = -lu;
|
||||
}
|
||||
if (mode == 2u && odd) {
|
||||
lv = -lv;
|
||||
}
|
||||
let ox = lu * cs - lv * sn;
|
||||
let oy = lu * sn + lv * cs;
|
||||
let tx = cx + ox;
|
||||
let ty = cy + oy;
|
||||
let sx = tx + (f32(p.x) + 0.5 - tx) * smear;
|
||||
let sy = ty + (f32(p.y) + 0.5 - ty) * smear;
|
||||
textureStore(out, p, sample_bilinear_clamped(src, sx, sy));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Mr. Smoothie (stylize3::smoothie)
|
||||
// aux = flow plane (x). f[0] = (ax, ay, bx, by); f[1] = (phase, loops).
|
||||
|
||||
fn fp2_pal(i: f32) -> vec4<f32> {
|
||||
let t = i / 255.0;
|
||||
let a = P.f[0].xy;
|
||||
let b = P.f[0].zw;
|
||||
let s = sample_bilinear_clamped(src, a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t);
|
||||
return vec4<f32>(fp2_unpremul(s), 1.0);
|
||||
}
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fp2_smoothie(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fp2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let px = textureLoad(src, p, 0);
|
||||
if (px.w <= 0.0) {
|
||||
textureStore(out, p, px);
|
||||
return;
|
||||
}
|
||||
let v = textureLoad(aux, p, 0).x;
|
||||
var t = fract_euclid(v * P.f[1].y + P.f[1].x);
|
||||
if (t < 0.5) {
|
||||
t = t * 2.0;
|
||||
} else {
|
||||
t = 2.0 - t * 2.0;
|
||||
}
|
||||
let f = t * 255.0;
|
||||
let i = min(floor(f), 254.0);
|
||||
let a = fp2_pal(i);
|
||||
let b = fp2_pal(i + 1.0);
|
||||
let c = a + (b - a) * (f - i);
|
||||
textureStore(out, p, vec4<f32>(c.xyz * px.w, px.w));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Inner/Outer Key (keying2)
|
||||
|
||||
// T = (straight colour, trimap) from src = the layer and aux = the trimap (x).
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fp2_io_prep(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fp2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
textureStore(out, p, vec4<f32>(fp2_unpremul(textureLoad(src, p, 0)), textureLoad(aux, p, 0).x));
|
||||
}
|
||||
|
||||
// A known region (u[0].x: 0 foreground, 1 background) of T: (colour × known, known).
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fp2_io_known(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fp2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let t = textureLoad(src, p, 0);
|
||||
var m = select(0.0, 1.0, t.w > 0.75);
|
||||
if (P.u[0].x == 1u) {
|
||||
m = select(0.0, 1.0, t.w < 0.25);
|
||||
}
|
||||
textureStore(out, p, vec4<f32>(t.xyz * m, m));
|
||||
}
|
||||
|
||||
// src = blurred foreground, aux = blurred background, data = T's rows (row length u[0].x):
|
||||
// the estimated matte with the decontaminated colours (colour, alpha).
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fp2_io_est(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fp2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let at = (u32(p.y) * P.u[0].x + u32(p.x)) * 4u;
|
||||
let c = vec3<f32>(data[at], data[at + 1u], data[at + 2u]);
|
||||
let tri = data[at + 3u];
|
||||
var a = tri;
|
||||
if (!(tri > 0.25 && tri < 0.75)) {
|
||||
textureStore(out, p, vec4<f32>(c, a));
|
||||
return;
|
||||
}
|
||||
let fs = textureLoad(src, p, 0);
|
||||
let bs = textureLoad(aux, p, 0);
|
||||
let fc = fs.xyz / max(fs.w, 1e-6);
|
||||
let bc = bs.xyz / max(bs.w, 1e-6);
|
||||
let d = fc - bc;
|
||||
let dd = d.x * d.x + d.y * d.y + d.z * d.z;
|
||||
if (dd < 1e-6) {
|
||||
a = 0.5;
|
||||
} else {
|
||||
a = clamp(((c.x - bc.x) * d.x + (c.y - bc.y) * d.y + (c.z - bc.z) * d.z) / dd, 0.0, 1.0);
|
||||
}
|
||||
var col = c;
|
||||
if (a > 0.02) {
|
||||
col = clamp((c - (1.0 - a) * bc) / a, vec3<f32>(0.0), vec3<f32>(1.0));
|
||||
}
|
||||
textureStore(out, p, vec4<f32>(col, a));
|
||||
}
|
||||
|
||||
// The matte (src.x, or src.w with u[0].y) after the Cleanup strokes: data = targets (u[0].x of
|
||||
// them) then each stroke's coverage plane (negative = untouched).
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fp2_io_strokes(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fp2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let s = textureLoad(src, p, 0);
|
||||
var a = select(s.x, s.w, P.u[0].y != 0u);
|
||||
let n = P.u[0].x;
|
||||
let d = out_dims();
|
||||
let plane = u32(d.x * d.y);
|
||||
let i = u32(p.y * d.x + p.x);
|
||||
for (var k = 0u; k < n; k++) {
|
||||
let c = data[n + k * plane + i];
|
||||
if (c >= 0.0) {
|
||||
a += (data[k] - a) * c;
|
||||
}
|
||||
}
|
||||
textureStore(out, p, vec4<f32>(a, 0.0, 0.0, 0.0));
|
||||
}
|
||||
|
||||
// src = the layer, aux = the final matte (x), data = estimated colours' rows (row length u[0].x,
|
||||
// colours used when u[0].y). u[0].z = invert; f[0] = (threshold, blend with original).
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fp2_io_final(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fp2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let px = textureLoad(src, p, 0);
|
||||
var m = textureLoad(aux, p, 0).x;
|
||||
let thr = P.f[0].x;
|
||||
if (thr > 0.0 && m < thr) {
|
||||
m = 0.0;
|
||||
}
|
||||
if (P.u[0].z != 0u) {
|
||||
m = 1.0 - m;
|
||||
}
|
||||
var c = fp2_unpremul(px);
|
||||
if (P.u[0].y != 0u) {
|
||||
let at = (u32(p.y) * P.u[0].x + u32(p.x)) * 4u;
|
||||
c = vec3<f32>(data[at], data[at + 1u], data[at + 2u]);
|
||||
}
|
||||
let a = max(px.w, 0.0);
|
||||
let keyed = vec4<f32>(c * (a * m), a * m);
|
||||
textureStore(out, p, keyed + (px - keyed) * P.f[0].y);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Simple Wire Removal (keying2::wire_removal)
|
||||
// u[0].x = style; f[0] = (ax, ay, nx, ny); f[1] = (hw thickness, soft width, mirror).
|
||||
|
||||
fn fp2_wire_pos(base: vec2<f32>, nrm: vec2<f32>, s: f32) -> vec4<f32> {
|
||||
return sample_bilinear_clamped(src, base.x + nrm.x * s, base.y + nrm.y * s);
|
||||
}
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fp2_wire(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fp2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let px = textureLoad(src, p, 0);
|
||||
let a = P.f[0].xy;
|
||||
let nrm = P.f[0].zw;
|
||||
let hw = P.f[1].x;
|
||||
let soft_w = P.f[1].y;
|
||||
let mirror = P.f[1].z;
|
||||
let fx = f32(p.x) + 0.5;
|
||||
let fy = f32(p.y) + 0.5;
|
||||
let q = vec2<f32>(fx - a.x, fy - a.y);
|
||||
let d = q.x * nrm.x + q.y * nrm.y;
|
||||
let ad = abs(d);
|
||||
if (ad > hw + soft_w) {
|
||||
textureStore(out, p, px);
|
||||
return;
|
||||
}
|
||||
let base = vec2<f32>(fx - d * nrm.x, fy - d * nrm.y);
|
||||
let edge = hw + 1.0;
|
||||
var repl: vec4<f32>;
|
||||
switch P.u[0].x {
|
||||
case 0u: {
|
||||
let t = clamp((d + edge) / (2.0 * edge), 0.0, 1.0);
|
||||
let l = fp2_wire_pos(base, nrm, -edge);
|
||||
repl = l + (fp2_wire_pos(base, nrm, edge) - l) * t;
|
||||
}
|
||||
case 1u, 2u: {
|
||||
var s = d - 2.0 * (hw - ad) - 1.0;
|
||||
if (d >= 0.0) {
|
||||
s = d + 2.0 * (hw - ad) + 1.0;
|
||||
}
|
||||
let sh = fp2_wire_pos(base, nrm, s);
|
||||
repl = sh;
|
||||
if (mirror > 0.0) {
|
||||
repl = sh + (fp2_wire_pos(base, nrm, -s) - sh) * (mirror * 0.5);
|
||||
}
|
||||
}
|
||||
default: {
|
||||
var sx = 0.0;
|
||||
if (abs(nrm.x) > 1e-6) {
|
||||
sx = (hw - ad + 1.0) / abs(nrm.x);
|
||||
}
|
||||
let dir = select(-1.0, 1.0, d * nrm.x >= 0.0);
|
||||
let sh = sample_bilinear_clamped(src, fx + dir * sx, fy);
|
||||
let mi = sample_bilinear_clamped(src, fx - dir * sx, fy);
|
||||
repl = sh;
|
||||
if (mirror > 0.0) {
|
||||
repl = sh + (mi - sh) * (mirror * 0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
var k = 1.0;
|
||||
if (ad > hw) {
|
||||
k = 1.0 - fp2_smoothstep(hw, hw + soft_w, ad);
|
||||
}
|
||||
textureStore(out, p, px + (repl - px) * k);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Basic 3D (obsolete::basic_3d)
|
||||
// u[0] = (specular, wireframe); f[0] = (cx, cy, focal, distance); f[1] = ux; f[2] = uy;
|
||||
// f[3] = normal; f[4] = light; f[5] = (hw width, hw height).
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fp2_basic3d(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fp2_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let cx = P.f[0].x;
|
||||
let cy = P.f[0].y;
|
||||
let f = P.f[0].z;
|
||||
let dist = P.f[0].w;
|
||||
let ux = P.f[1].xyz;
|
||||
let uy = P.f[2].xyz;
|
||||
let n = P.f[3].xyz;
|
||||
let d = vec3<f32>(f32(p.x) + 0.5 - cx, f32(p.y) + 0.5 - cy, f);
|
||||
let denom = dot(d, n);
|
||||
if (abs(denom) < 1e-9) {
|
||||
textureStore(out, p, vec4<f32>(0.0));
|
||||
return;
|
||||
}
|
||||
// Eye at (0, 0, -f), plane through (0, 0, dist).
|
||||
let t = (dist + f) * n.z / denom;
|
||||
if (t <= 0.0) {
|
||||
textureStore(out, p, vec4<f32>(0.0));
|
||||
return;
|
||||
}
|
||||
let hit = vec3<f32>(d.x * t, d.y * t, -f + d.z * t - dist);
|
||||
let u = dot(hit, ux);
|
||||
let v = dot(hit, uy);
|
||||
var c = sample_bilinear(src, cx + u, cy + v);
|
||||
if (P.u[0].x != 0u && c.w > 0.0) {
|
||||
let dv = d / sqrt(dot(d, d));
|
||||
let k = 2.0 * dot(dv, n);
|
||||
let r = dv - k * n;
|
||||
let s = powz(max(-dot(r, P.f[4].xyz), 0.0), 40.0);
|
||||
let a = c.w;
|
||||
c = vec4<f32>(c.xyz + s * a * max(vec3<f32>(1.0) - c.xyz / max(a, 1e-6), vec3<f32>(0.0)), a);
|
||||
}
|
||||
if (P.u[0].y != 0u) {
|
||||
let hw = P.f[5].x;
|
||||
let hh = P.f[5].y;
|
||||
let e = min(abs(abs(u) - hw), abs(abs(v) - hh));
|
||||
if (e < 0.75 && abs(u) <= hw + 0.75 && abs(v) <= hh + 0.75) {
|
||||
c = vec4<f32>(1.0);
|
||||
}
|
||||
}
|
||||
textureStore(out, p, c);
|
||||
}
|
||||
@@ -57,7 +57,7 @@ fn gpu_adjustment_chains_match_the_cpu() {
|
||||
#[test]
|
||||
fn mixed_adjustment_chains_fall_back_per_effect() {
|
||||
let mut stack = gpu_stack();
|
||||
stack.insert(1, ("ec.stylize.ccplastic", vec![]));
|
||||
stack.insert(1, ("ec.distort.rollingshutterrepair", vec![]));
|
||||
stack.push(("ec.stylize.glow", vec![("threshold", n(30.0))]));
|
||||
for depth in [BitDepth::Bpc8, BitDepth::Bpc32] {
|
||||
let s = adjustment_scene(depth, &stack, true);
|
||||
@@ -93,7 +93,7 @@ impl FxTarget for Probe {
|
||||
fn adjustment_stacks_split_into_gpu_runs_and_cpu_steps() {
|
||||
let Some(g) = gpu() else { return };
|
||||
let mut stack = gpu_stack();
|
||||
stack.insert(1, ("ec.stylize.ccplastic", vec![]));
|
||||
stack.insert(1, ("ec.generate.fractal", vec![]));
|
||||
let s = adjustment_scene(BitDepth::Bpc32, &stack, false);
|
||||
let mut r = Renderer::new(&s.p, &Pattern, RenderOpts { backend: Backend::Gpu, ..opts() });
|
||||
r.accel = Some(g);
|
||||
|
||||
@@ -466,7 +466,7 @@ fn gpu_compositor_draws_split_advanced_3d_runs_and_skies() {
|
||||
// A varying environment image (so the sky's lookup shows; a CPU effect, so the frame's
|
||||
// only readback is its own).
|
||||
let mut next = p.next_id;
|
||||
let spec = effectcraft_effects::find("ec.generate.lensflare").unwrap();
|
||||
let spec = effectcraft_effects::find("ec.generate.fractal").unwrap();
|
||||
let ramp = effectcraft_effects::instantiate(spec, &mut build::Ids(&mut next), spec.name, [32.0, 16.0]);
|
||||
p.next_id = next;
|
||||
if case == 3 {
|
||||
|
||||
@@ -26,7 +26,7 @@ pub(crate) fn deferred_gpu() -> Option<(&'static Gpu, std::sync::MutexGuard<'sta
|
||||
/// 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).
|
||||
pub(crate) fn scene() -> Scene {
|
||||
let cpu_only = "ec.blur.ccradial";
|
||||
let cpu_only = "ec.generate.fractal";
|
||||
assert!(!effectcraft_effects::GPU_EFFECTS.contains(&cpu_only));
|
||||
let mut s = Scene::new(BitDepth::Bpc32);
|
||||
let mut bg = s.footage(97, 61);
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
//! GPU generators of part C (Lightning, Advanced Lightning, Beam, Lens Flare, Radio
|
||||
//! Waves, Vegas, Stroke, Scribble, Write-on, Paint Bucket, Eyedropper Fill, CC Glue Gun, CC
|
||||
//! Threads, Audio Spectrum, Audio Waveform, Basic Text, Path Text) vs the CPU effects (the
|
||||
//! oracle): direct on a buffer with masks, another layer and audio at full and half resolution
|
||||
//! and as adjustment, and composited at 8 and 32 bpc.
|
||||
|
||||
use effectcraft_keyframe::Value;
|
||||
use effectcraft_time::Tick;
|
||||
|
||||
use crate::tests::{c, n};
|
||||
use crate::tests_fx_pixel2::{e, env_case, env_case_allow, env_case_at, off, on, pt};
|
||||
|
||||
fn layer(id: u64) -> Value {
|
||||
Value::Layer(Some(id))
|
||||
}
|
||||
|
||||
fn text(s: &str) -> Value {
|
||||
Value::Str(s.into())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lightning() {
|
||||
env_case("ec.obsolete.lightning", &[]);
|
||||
env_case(
|
||||
"ec.obsolete.lightning",
|
||||
&[("branching", n(0.9)), ("rebranching", n(0.6)), ("detailLevel", n(4.0)), ("blendingMode", e(1)), ("pullForce", n(40.0))],
|
||||
);
|
||||
env_case("ec.obsolete.lightning", &[("blendingMode", e(2)), ("rerunAtEachFrame", on()), ("fixedEndpoint", on()), ("coreWidth", n(0.8))]);
|
||||
env_case("ec.generate.advancedlightning", &[]);
|
||||
env_case("ec.generate.advancedlightning", &[("lightningType", e(4)), ("forking", n(60.0)), ("decayMainCore", on()), ("expertSettings/coreDrain", n(40.0))]);
|
||||
env_case(
|
||||
"ec.generate.advancedlightning",
|
||||
&[("alphaObstacle", n(6.0)), ("compositeOnOriginal", off()), ("expertSettings/fractalType", e(2)), ("glowSettings/glowRadius", n(10.0))],
|
||||
);
|
||||
env_case("ec.generate.advancedlightning", &[("lightningType", e(7)), ("glowSettings/glowRadius", n(0.0)), ("coreSettings/coreRadius", n(1.5))]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn closed_form() {
|
||||
env_case("ec.generate.beam", &[]);
|
||||
env_case("ec.generate.beam", &[("length", n(70.0)), ("time", n(40.0)), ("softness", n(0.0)), ("perspective3d", off()), ("compositeOnOriginal", off())]);
|
||||
env_case("ec.generate.beam", &[("startThickness", n(3.0)), ("endThickness", n(20.0)), ("insideColor", c(0.3, 0.9, 1.0))]);
|
||||
env_case("ec.generate.lensflare", &[]);
|
||||
env_case("ec.generate.lensflare", &[("lensType", e(1)), ("flareCenter", pt(50.0, 30.0)), ("blendWithOriginal", n(30.0))]);
|
||||
env_case("ec.generate.lensflare", &[("lensType", e(2)), ("flareBrightness", n(160.0))]);
|
||||
env_case("ec.generate.ccgluegun", &[]);
|
||||
env_case("ec.generate.ccgluegun", &[("density", n(12.0)), ("strokeWidth", n(40.0)), ("style", e(1)), ("strength", n(60.0))]);
|
||||
let t = |vals: &[(&str, Value)]| env_case_allow("ec.generate.ccthreads", vals, THREAD_TIES);
|
||||
t(&[]);
|
||||
t(&[("texture", n(0.0))]);
|
||||
// Generic values: no ties.
|
||||
env_case("ec.generate.ccthreads", &[("width", n(13.1)), ("height", n(17.3)), ("overlaps", n(2.0)), ("direction", n(23.0)), ("texture", n(0.0))]);
|
||||
env_case(
|
||||
"ec.generate.ccthreads",
|
||||
&[("width", n(9.3)), ("height", n(11.1)), ("coverage", n(55.3)), ("shadowing", n(90.0)), ("texture", n(40.0)), ("center", pt(33.7, 21.9))],
|
||||
);
|
||||
}
|
||||
|
||||
/// CC Threads decides per pixel which thread band it lies in and which fibre (the position
|
||||
/// across / along the thread times 8 / 64, truncated) shades it: at parameter values that put
|
||||
/// pixel centres exactly on a band edge or fibre boundary (the defaults do), f32 and the CPU's
|
||||
/// f64 can land on either side.
|
||||
const THREAD_TIES: f64 = 0.05;
|
||||
|
||||
#[test]
|
||||
fn radio_waves() {
|
||||
let t = Tick::from_seconds_f64(1.3);
|
||||
env_case_at("ec.generate.radiowaves", &[("waveMotion/expansion", n(30.0))], 0.0, t);
|
||||
env_case_at(
|
||||
"ec.generate.radiowaves",
|
||||
&[
|
||||
("polygon/sides", n(5.0)),
|
||||
("polygon/star", on()),
|
||||
("waveMotion/spin", n(30.0)),
|
||||
("waveStroke/profile", e(4)),
|
||||
("waveMotion/velocity", n(20.0)),
|
||||
("waveMotion/expansion", n(25.0)),
|
||||
],
|
||||
0.0,
|
||||
t,
|
||||
);
|
||||
env_case_at(
|
||||
"ec.generate.radiowaves",
|
||||
&[("waveType", e(1)), ("imageContour/sourceLayer", layer(9)), ("waveStroke/fadeOutTime", n(1.0)), ("waveMotion/expansion", n(10.0))],
|
||||
0.0,
|
||||
t,
|
||||
);
|
||||
env_case_at(
|
||||
"ec.generate.radiowaves",
|
||||
&[
|
||||
("waveType", e(2)),
|
||||
("waveMask/mask", n(1.0)),
|
||||
("waveMotion/reflection", on()),
|
||||
("waveMotion/velocity", n(40.0)),
|
||||
("waveMotion/expansion", n(15.0)),
|
||||
("waveStroke/profile", e(5)),
|
||||
],
|
||||
0.0,
|
||||
t,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strokes() {
|
||||
env_case("ec.generate.stroke", &[]);
|
||||
env_case("ec.generate.stroke", &[("allMasks", on()), ("spacing", n(80.0)), ("paintStyle", e(1)), ("end", n(70.0)), ("brushSize", n(6.0))]);
|
||||
env_case("ec.generate.stroke", &[("path", n(3.0)), ("paintStyle", e(2)), ("start", n(20.0)), ("brushHardness", n(10.0))]);
|
||||
env_case("ec.generate.scribble", &[]);
|
||||
env_case("ec.generate.scribble", &[("scribble", e(1)), ("fillType", e(2)), ("edgeOptions/endCap", e(2)), ("edgeOptions/join", e(1)), ("composite", e(0))]);
|
||||
env_case(
|
||||
"ec.generate.scribble",
|
||||
&[("mask", n(2.0)), ("fillType", e(1)), ("edgeOptions/join", e(2)), ("wiggleType", e(2)), ("end", n(60.0)), ("composite", e(2))],
|
||||
);
|
||||
env_case("ec.generate.scribble", &[("mask", n(3.0))]);
|
||||
env_case("ec.generate.vegas", &[]);
|
||||
env_case("ec.generate.vegas", &[("stroke", e(1)), ("path", n(3.0)), ("blendMode", e(2)), ("segments", n(12.0)), ("randomPhase", on())]);
|
||||
env_case("ec.generate.vegas", &[("inputLayer", layer(9)), ("blendMode", e(3)), ("shorterContoursHave", e(1)), ("threshold", n(30.0))]);
|
||||
env_case("ec.generate.vegas", &[("stroke", e(1)), ("path", n(1.0)), ("blendMode", e(0)), ("width", n(5.0)), ("hardness", n(0.9))]);
|
||||
env_case("ec.generate.writeon", &[]);
|
||||
env_case("ec.generate.writeon", &[("brushPosition", pt(20.0, 30.0)), ("brushSize", n(14.0)), ("paintStyle", e(1))]);
|
||||
env_case("ec.generate.writeon", &[("paintStyle", e(2)), ("brushHardness", n(20.0)), ("brushOpacity", n(60.0))]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fills() {
|
||||
env_case("ec.generate.paintbucket", &[]);
|
||||
env_case(
|
||||
"ec.generate.paintbucket",
|
||||
&[("fillPoint", pt(10.0, 10.0)), ("fillSelector", e(1)), ("tolerance", n(60.0)), ("stroke", e(4)), ("strokeWidth", n(4.0))],
|
||||
);
|
||||
env_case("ec.generate.paintbucket", &[("fillPoint", pt(35.0, 22.0)), ("fillSelector", e(2)), ("stroke", e(1)), ("color", c(0.2, 0.9, 0.4))]);
|
||||
env_case("ec.generate.paintbucket", &[("stroke", e(2)), ("spreadRadius", n(2.5)), ("tolerance", n(120.0)), ("blendingMode", e(5))]);
|
||||
env_case("ec.generate.paintbucket", &[("stroke", e(3)), ("tolerance", n(90.0)), ("blendingMode", e(9))]);
|
||||
env_case("ec.generate.paintbucket", &[("viewThreshold", on()), ("tolerance", n(80.0))]);
|
||||
env_case("ec.generate.paintbucket", &[("tolerance", n(100.0)), ("blendingMode", e(8)), ("opacity", n(70.0))]);
|
||||
env_case("ec.generate.eyedropperfill", &[]);
|
||||
env_case(
|
||||
"ec.generate.eyedropperfill",
|
||||
&[("samplePoint", pt(12.0, 30.0)), ("sampleRadius", n(6.0)), ("averagePixelColors", e(3)), ("blendWithOriginal", n(30.0))],
|
||||
);
|
||||
env_case("ec.generate.eyedropperfill", &[("sampleRadius", n(9.0)), ("averagePixelColors", e(1)), ("maintainOriginalAlpha", on())]);
|
||||
env_case("ec.generate.eyedropperfill", &[("sampleRadius", n(4.0)), ("averagePixelColors", e(2))]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn audio() {
|
||||
let a = || ("audioLayer", layer(9));
|
||||
env_case("ec.generate.audiospectrum", &[a()]);
|
||||
env_case("ec.generate.audiospectrum", &[a(), ("displayOptions", e(1)), ("sideOptions", e(2)), ("compositeOnOriginal", on())]);
|
||||
env_case("ec.generate.audiospectrum", &[a(), ("displayOptions", e(2)), ("path", n(3.0)), ("softness", n(0.0))]);
|
||||
env_case("ec.generate.audiowaveform", &[a()]);
|
||||
env_case("ec.generate.audiowaveform", &[a(), ("displayOptions", e(0)), ("waveformOptions", e(2)), ("usePolarPath", on())]);
|
||||
env_case("ec.generate.audiowaveform", &[a(), ("displayOptions", e(2)), ("path", n(1.0)), ("compositeOnOriginal", on())]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_effects() {
|
||||
env_case("ec.obsolete.basictext", &[]);
|
||||
env_case("ec.obsolete.basictext", &[("text", text("Hi\nthere")), ("fillAndStroke/displayOptions", e(2)), ("alignment", e(0)), ("size", n(14.0))]);
|
||||
env_case("ec.obsolete.basictext", &[("fillAndStroke/displayOptions", e(3)), ("fillAndStroke/strokeWidth", n(5.0)), ("compositeOnOriginal", on())]);
|
||||
env_case("ec.obsolete.basictext", &[("fillAndStroke/displayOptions", e(1)), ("fillAndStroke/strokeWidth", n(1.0)), ("size", n(20.0))]);
|
||||
env_case("ec.obsolete.pathtext", &[]);
|
||||
env_case(
|
||||
"ec.obsolete.pathtext",
|
||||
&[("pathOptions/shapeType", e(1)), ("advanced/visibleCharacters", n(2.5)), ("advanced/fadeTime", n(50.0)), ("character/size", n(14.0))],
|
||||
);
|
||||
env_case(
|
||||
"ec.obsolete.pathtext",
|
||||
&[
|
||||
("pathOptions/customPath", n(3.0)),
|
||||
("fillAndStroke/displayOptions", e(2)),
|
||||
("advanced/jitterSettings/rotationJitterMax", n(20.0)),
|
||||
("compositeOnOriginal", on()),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
//! GPU pixel ports of part C (CC Cross / Radial / Vector Blur, Reduce Interlace Flicker, CC
|
||||
//! Composite, Color Link, Cineon Converter, HDR Compander, HDR Highlight Compression, Grow
|
||||
//! Bounds, CC Overbrights, CC Block Load, CC Burn Film, CC Glass, CC HexTile, CC Mr. Smoothie,
|
||||
//! CC Plastic, Inner/Outer Key, CC Simple Wire Removal, Basic 3D) vs the CPU effects (the
|
||||
//! oracle): direct on a buffer at full and half resolution, as adjustment, and composited at 8
|
||||
//! and 32 bpc. Also the helpers for effects that read masks, other layers and audio.
|
||||
|
||||
use effectcraft_effects::{Buf, EffectEnv, EffectHost, LayerPixels, MaskShape};
|
||||
use effectcraft_keyframe::{ShapePath, Value};
|
||||
use effectcraft_project::build::{self, Ids};
|
||||
use effectcraft_project::{BitDepth, MaskMode};
|
||||
use effectcraft_render::RenderOpts;
|
||||
use effectcraft_time::Tick;
|
||||
|
||||
use crate::tests::{Scene, c, check, compare_at, diff, effect_case, gpu, n, opts, pattern, set};
|
||||
|
||||
pub(crate) fn e(v: u32) -> Value {
|
||||
Value::Enum(v)
|
||||
}
|
||||
|
||||
pub(crate) fn on() -> Value {
|
||||
Value::Bool(true)
|
||||
}
|
||||
|
||||
pub(crate) fn off() -> Value {
|
||||
Value::Bool(false)
|
||||
}
|
||||
|
||||
pub(crate) fn pt(x: f64, y: f64) -> Value {
|
||||
Value::Vec2([x, y])
|
||||
}
|
||||
|
||||
/// Size of the test layer (layer pixels).
|
||||
const SIZE: [f64; 2] = [70.0, 44.0];
|
||||
|
||||
/// The test layer's masks (layer pixels): a closed pentagon, a closed quad and an open curve.
|
||||
pub(crate) fn mask_points() -> Vec<(Vec<[f64; 2]>, bool)> {
|
||||
let pent = (0..5)
|
||||
.map(|i| {
|
||||
let a = i as f64 * std::f64::consts::TAU / 5.0 - 1.3;
|
||||
[33.3 + a.cos() * 17.1, 22.4 + a.sin() * 15.2]
|
||||
})
|
||||
.collect();
|
||||
vec![
|
||||
(pent, true),
|
||||
(vec![[6.3, 5.1], [62.2, 7.7], [59.4, 39.6], [9.1, 37.2]], true),
|
||||
(vec![[4.2, 30.3], [18.7, 12.4], [33.1, 28.9], [47.6, 9.8], [64.2, 25.5]], false),
|
||||
]
|
||||
}
|
||||
|
||||
/// Another layer (`id` 9) and an audio track for layer parameters.
|
||||
pub(crate) struct Host;
|
||||
impl EffectHost for Host {
|
||||
fn layer(&self, id: u64, _: bool) -> Option<LayerPixels> {
|
||||
Some(LayerPixels { buf: Buf { img: pattern(id as u32 + 3, 70, 44), offset: [0.0; 2], scale: 1.0 }, size: SIZE })
|
||||
}
|
||||
fn audio(&self, _: u64, start: f64, frames: usize, rate: u32) -> Option<Vec<f32>> {
|
||||
Some(
|
||||
(0..frames)
|
||||
.flat_map(|i| {
|
||||
let t = start + i as f64 / rate as f64;
|
||||
let l = 0.5 * (t * 440.0 * std::f64::consts::TAU).sin() + 0.2 * (t * 1300.0 * std::f64::consts::TAU).sin();
|
||||
let r = 0.4 * (t * (200.0 + 300.0 * t) * std::f64::consts::TAU).sin();
|
||||
[l as f32, r as f32]
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// The effect alone on a buffer with the test masks and [`Host`]: GPU vs CPU (32-bit float)
|
||||
/// at full and half resolution (the half buffer padded and offset), direct and as adjustment.
|
||||
/// `allow` = fraction of pixels allowed over 1e-3.
|
||||
pub(crate) fn direct_env(id: &str, vals: &[(&str, Value)], allow: f64) {
|
||||
let Some(g) = gpu() else { return };
|
||||
let spec = effectcraft_effects::find(id).unwrap();
|
||||
let mut params =
|
||||
effectcraft_effects::Params { values: spec.params.iter().map(|ps| (ps.id.to_string(), effectcraft_effects::default_value(ps, SIZE))).collect() };
|
||||
for (k, v) in vals {
|
||||
assert!(params.values.contains_key(*k), "{id}: no {k}");
|
||||
params.values.insert(k.to_string(), v.clone());
|
||||
}
|
||||
let masks: Vec<MaskShape> = mask_points()
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, (points, closed))| MaskShape { name: format!("Mask {}", i + 1), points, closed, inverted: false })
|
||||
.collect();
|
||||
let host = Host;
|
||||
for adjustment in [false, true] {
|
||||
for scale in [1.0, 0.5] {
|
||||
let img = pattern(7, (SIZE[0] * scale) as u32, (SIZE[1] * scale) as u32);
|
||||
let mut buf = Buf { img, offset: [0.0, 0.0], scale };
|
||||
if scale < 1.0 {
|
||||
buf.pad(3);
|
||||
}
|
||||
let env = EffectEnv { masks: &masks, host: Some(&host), comp_time: 0.25, frame_rate: 30.0, ..Default::default() };
|
||||
let ctx = || effectcraft_effects::EffectCtx { params: ¶ms, time: 0.25, layer_size: SIZE, seed: 11, adjustment, env };
|
||||
let cpu = (spec.render)(&ctx(), buf.clone());
|
||||
let out =
|
||||
effectcraft_render::Accelerator::effects(g, &[effectcraft_render::FxStep { spec, ctx: ctx() }], &buf, None).expect("the GPU runs the effect");
|
||||
assert_eq!((out.offset, out.scale), (cpu.offset, cpu.scale), "{id}: geometry");
|
||||
let d = diff(&cpu.img, &out.img, 1e-3);
|
||||
assert!(
|
||||
d.over as f64 <= allow * d.total as f64,
|
||||
"{id} {vals:?} adj {adjustment} scale {scale}: {} of {} pixels over 1e-3 (max {}); worst {:?}",
|
||||
d.over,
|
||||
d.total,
|
||||
d.max,
|
||||
d.worst
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// [`direct_env`], then a footage layer with the test masks (mode None) and the effect,
|
||||
/// composited over a background at 8 and 32 bpc, full and half resolution.
|
||||
pub(crate) fn env_case(id: &str, vals: &[(&str, Value)]) {
|
||||
env_case_allow(id, vals, 0.0);
|
||||
}
|
||||
|
||||
/// [`env_case`] allowing a fraction of pixels over the tolerance (documented exceptions).
|
||||
pub(crate) fn env_case_allow(id: &str, vals: &[(&str, Value)], allow: f64) {
|
||||
env_case_at(id, vals, allow, Tick::ZERO);
|
||||
}
|
||||
|
||||
/// [`env_case_allow`] with the composited frames at comp time `t`.
|
||||
pub(crate) fn env_case_at(id: &str, vals: &[(&str, Value)], allow: f64, t: Tick) {
|
||||
direct_env(id, vals, allow);
|
||||
for depth in [BitDepth::Bpc8, BitDepth::Bpc32] {
|
||||
let mut s = Scene::new(depth);
|
||||
let bg = s.solid([0.15, 0.1, 0.2], 97, 61);
|
||||
s.push(bg);
|
||||
let mut l = s.footage(70, 44);
|
||||
let mut next = s.p.next_id;
|
||||
for (i, (pts, closed)) in mask_points().into_iter().enumerate() {
|
||||
let m = build::mask(&mut Ids(&mut next), &format!("Mask {}", i + 1), ShapePath::polygon(&pts, closed), MaskMode::None, [255, 255, 0]);
|
||||
l.props.sub_mut("masks").unwrap().children.push(m.into());
|
||||
}
|
||||
s.p.next_id = next;
|
||||
s.effect(&mut l, id, vals);
|
||||
set(&mut l, "transform/rotation", Value::Scalar(8.0));
|
||||
s.push(l);
|
||||
check(&format!("{id} {vals:?} {depth:?}"), compare_at(&s, opts(), t), allow);
|
||||
check(&format!("{id} {vals:?} {depth:?} half"), compare_at(&s, RenderOpts { scale: 0.5, ..opts() }, t), allow);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blurs() {
|
||||
effect_case("ec.blur.cccross", &[]);
|
||||
effect_case("ec.blur.cccross", &[("radiusX", n(4.0)), ("radiusY", n(0.0)), ("transferMode", e(1))]);
|
||||
effect_case("ec.blur.cccross", &[("radiusX", n(3.0)), ("radiusY", n(7.0)), ("transferMode", e(2))]);
|
||||
effect_case("ec.blur.cccross", &[("radiusX", n(6.0)), ("radiusY", n(2.0)), ("transferMode", e(3))]);
|
||||
effect_case("ec.blur.ccradial", &[]);
|
||||
effect_case("ec.blur.ccradial", &[("type", e(0)), ("amount", n(-40.0)), ("center", pt(20.0, 30.0))]);
|
||||
effect_case("ec.blur.ccradial", &[("type", e(2)), ("amount", n(60.0)), ("quality", n(20.0))]);
|
||||
effect_case("ec.blur.ccradial", &[("type", e(3)), ("amount", n(30.0))]);
|
||||
effect_case("ec.blur.ccradial", &[("type", e(4)), ("amount", n(-50.0)), ("quality", n(80.0))]);
|
||||
effect_case("ec.blur.ccvector", &[]);
|
||||
effect_case("ec.blur.ccvector", &[("type", e(1)), ("amount", n(6.0)), ("angleOffset", n(30.0))]);
|
||||
effect_case("ec.blur.ccvector", &[("type", e(2)), ("mapSoftness", n(0.0))]);
|
||||
effect_case("ec.blur.ccvector", &[("type", e(3)), ("amount", n(5.0))]);
|
||||
effect_case("ec.blur.ccvector", &[("type", e(4)), ("amount", n(12.0)), ("mapSoftness", n(40.0))]);
|
||||
effect_case("ec.blur.reduceflicker", &[("softness", n(3.0))]);
|
||||
effect_case("ec.blur.reduceflicker", &[("softness", n(0.5))]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn composites_and_colour() {
|
||||
for m in [0, 1, 2, 3, 5, 8, 11, 14, 15] {
|
||||
effect_case("ec.channel.cccomposite", &[("compositeOriginal", e(m)), ("opacity", n(70.0))]);
|
||||
}
|
||||
effect_case("ec.channel.cccomposite", &[("compositeOriginal", e(4)), ("rgbOnly", on())]);
|
||||
effect_case("ec.color.colorlink", &[]);
|
||||
effect_case("ec.color.colorlink", &[("sampleSource", e(2)), ("clip", n(10.0)), ("blendingMode", e(3)), ("opacity", n(60.0))]);
|
||||
effect_case("ec.color.colorlink", &[("sampleSource", e(1)), ("blendingMode", e(11)), ("stencilOriginalAlpha", off())]);
|
||||
effect_case("ec.color.colorlink", &[("sampleSource", e(9)), ("clip", n(20.0))]);
|
||||
effect_case("ec.color.colorlink", &[("sampleSource", e(6)), ("opacity", n(50.0)), ("stencilOriginalAlpha", off())]);
|
||||
env_case("ec.color.colorlink", &[("sourceLayer", Value::Layer(Some(9))), ("sampleSource", e(4)), ("clip", n(5.0))]);
|
||||
effect_case("ec.utility.cineon", &[]);
|
||||
effect_case("ec.utility.cineon", &[("conversionType", e(0)), ("gamma", n(2.2))]);
|
||||
effect_case("ec.utility.cineon", &[("conversionType", e(2)), ("highlightRolloff", n(60.0)), ("tenBitWhitePoint", n(800.0))]);
|
||||
effect_case("ec.utility.hdrcompander", &[("gain", n(4.0)), ("gamma", n(2.0))]);
|
||||
effect_case("ec.utility.hdrcompander", &[("mode", e(1)), ("gain", n(3.0)), ("gamma", n(1.5))]);
|
||||
effect_case("ec.utility.hdrcompression", &[]);
|
||||
effect_case("ec.utility.hdrcompression", &[("amount", n(40.0))]);
|
||||
effect_case("ec.utility.growbounds", &[("pixels", n(9.0))]);
|
||||
effect_case("ec.utility.ccoverbrights", &[]);
|
||||
for ch in 1..=5 {
|
||||
effect_case("ec.utility.ccoverbrights", &[("channel", e(ch)), ("highlightColor", c(0.2, 0.9, 0.4))]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stylize() {
|
||||
for (done, flags) in
|
||||
[(30.0, [true, true, true, false]), (55.0, [false, true, false, true]), (70.0, [true, false, false, false]), (10.0, [false, false, true, true])]
|
||||
{
|
||||
effect_case(
|
||||
"ec.stylize.ccblockload",
|
||||
&[
|
||||
("completion", n(done)),
|
||||
("scanlines", Value::Bool(flags[0])),
|
||||
("smoothing", Value::Bool(flags[1])),
|
||||
("startCleared", Value::Bool(flags[2])),
|
||||
("bilinear", Value::Bool(flags[3])),
|
||||
],
|
||||
);
|
||||
}
|
||||
effect_case("ec.stylize.ccburnfilm", &[("burn", n(30.0))]);
|
||||
effect_case("ec.stylize.ccburnfilm", &[("burn", n(60.0)), ("center", pt(20.0, 15.0)), ("randomSeed", n(4.0))]);
|
||||
effect_case("ec.stylize.ccglass", &[]);
|
||||
effect_case("ec.stylize.ccglass", &[("lightType", e(1)), ("displacement", n(-60.0)), ("property", e(4)), ("softness", n(4.0))]);
|
||||
effect_case("ec.stylize.ccglass", &[("height", n(0.0)), ("lightColor", c(1.0, 0.8, 0.5)), ("metal", n(60.0))]);
|
||||
effect_case("ec.stylize.ccplastic", &[]);
|
||||
effect_case("ec.stylize.ccplastic", &[("lightType", e(1)), ("cutMin", n(20.0)), ("cutMax", n(70.0)), ("property", e(5))]);
|
||||
env_case("ec.stylize.ccplastic", &[("bumpLayer", Value::Layer(Some(9))), ("property", e(0)), ("roughness", n(0.2))]);
|
||||
effect_case("ec.stylize.cchextile", &[("radius", n(9.3))]);
|
||||
effect_case("ec.stylize.cchextile", &[("radius", n(7.7)), ("render", e(1)), ("rotate", n(17.0)), ("smearing", n(30.0))]);
|
||||
effect_case("ec.stylize.cchextile", &[("radius", n(12.1)), ("render", e(2)), ("center", pt(31.3, 19.7))]);
|
||||
effect_case("ec.stylize.ccmrsmoothie", &[]);
|
||||
effect_case("ec.stylize.ccmrsmoothie", &[("property", e(6)), ("smoothness", n(5.0)), ("colorLoop", e(2)), ("phase", n(40.0))]);
|
||||
env_case("ec.stylize.ccmrsmoothie", &[("flowLayer", Value::Layer(Some(9))), ("property", e(1))]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keys_and_3d() {
|
||||
env_case("ec.key.innerouter", &[]);
|
||||
env_case("ec.key.innerouter", &[("foreground", e(1))]);
|
||||
direct_env("ec.key.innerouter", &[("foreground", e(1)), ("background", e(2))], 0.0);
|
||||
env_case("ec.key.innerouter", &[("foreground", e(1)), ("background", e(2)), ("edgeFeather", n(3.0)), ("edgeThin", n(1.5))]);
|
||||
env_case("ec.key.innerouter", &[("foreground", e(1)), ("singleMaskHighlightRadius", n(4.0)), ("invertExtraction", on()), ("blendWithOriginal", n(20.0))]);
|
||||
env_case(
|
||||
"ec.key.innerouter",
|
||||
&[
|
||||
("foreground", e(2)),
|
||||
("edgeThin", n(-2.3)),
|
||||
("edgeThreshold", n(30.0)),
|
||||
("cleanupForeground/cleanupForeground1/path", e(3)),
|
||||
("cleanupBackground/cleanupBackground1/path", e(1)),
|
||||
],
|
||||
);
|
||||
effect_case("ec.key.ccsimplewireremoval", &[]);
|
||||
effect_case("ec.key.ccsimplewireremoval", &[("removalStyle", e(0)), ("thickness", n(8.0)), ("slope", n(50.0))]);
|
||||
effect_case("ec.key.ccsimplewireremoval", &[("removalStyle", e(1)), ("mirrorBlend", n(60.0)), ("pointB", pt(60.0, 10.0))]);
|
||||
effect_case("ec.key.ccsimplewireremoval", &[("removalStyle", e(3)), ("mirrorBlend", n(30.0)), ("slope", n(20.0))]);
|
||||
effect_case("ec.obsolete.basic3d", &[("swivel", n(25.0))]);
|
||||
effect_case("ec.obsolete.basic3d", &[("tilt", n(-30.0)), ("swivel", n(10.0)), ("specularHighlight", on())]);
|
||||
effect_case("ec.obsolete.basic3d", &[("distanceToImage", n(20.0)), ("preview/drawPreviewWireframe", on()), ("tilt", n(15.0))]);
|
||||
}
|
||||
Reference in New Issue
Block a user