mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-14 19:27:05 -04:00
Merge branch 'worktree-agent-a38a174c36aa9f23a'
This commit is contained in:
commit
7a5a93bcbc
32 files changed
+3565
-174
No files matched your search
@@ -94,12 +94,14 @@ const CPU_ONLY_CONTROLS: &[(&str, &[&str])] = &[
|
||||
];
|
||||
|
||||
/// Whether the GPU kernel of effect `id` covers the instance's current settings (see
|
||||
/// [`CPU_ONLY_CONTROLS`]; plus Cell Pattern's HQ variants and Turbulent Displace's locked
|
||||
/// pinning).
|
||||
/// [`CPU_ONLY_CONTROLS`]; plus Cell Pattern's HQ variants, Turbulent Displace's locked
|
||||
/// pinning, Warp's strong Fisheye / Twist and Timewarp with a Matte Layer).
|
||||
pub fn gpu_supported(id: &str, ctx: &crate::EffectCtx) -> bool {
|
||||
match id {
|
||||
"ec.generate.cellpattern" if (6..=10).contains(&ctx.params.e("cellPattern")) => return false,
|
||||
"ec.distort.turbulentdisplace" if ctx.params.e("pinning") >= 8 => return false,
|
||||
// Timewarp's Matte Layer (foreground and background on their own vectors).
|
||||
"ec.time.timewarp" if ctx.params.get("matteLayer").and_then(effectcraft_keyframe::Value::as_layer).is_some() => return false,
|
||||
// Fisheye / Twist (a crease at the unit circle) bent past 50 % or with Horizontal /
|
||||
// Vertical Distortion: Newton's inverse wanders chaotically before converging near the
|
||||
// fold, so f32 lands on other pixels than the CPU's f64.
|
||||
|
||||
@@ -94,7 +94,8 @@ pub use transition::place_layer;
|
||||
// effectcraft-gpu fx_noise.
|
||||
pub use noise::remove_grain_level;
|
||||
pub use noise3::{FractalGpu, fractal_gpu};
|
||||
pub use time_fx::{posterized_time, time_frames};
|
||||
pub use textfx::{TextLayer, text_layer};
|
||||
pub use time_fx::{PixelMotion, TimewarpPlan, TwStep, posterized_time, time_frames, timewarp_crop, timewarp_plan};
|
||||
// effectcraft-gpu fx_tone.
|
||||
pub use ocio::color_stabilizer_maps;
|
||||
|
||||
@@ -931,6 +932,40 @@ pub const GPU_EFFECTS: &[&str] = &[
|
||||
"ec.perspective.bevelalpha",
|
||||
"ec.perspective.beveledges",
|
||||
"ec.obsolete.gaussianlegacy",
|
||||
// effectcraft-gpu fx_light (the CC light family)
|
||||
"ec.generate.cclightrays",
|
||||
"ec.generate.cclightburst",
|
||||
"ec.generate.cclightsweep",
|
||||
"ec.transition.cclightwipe",
|
||||
// effectcraft-gpu fx_transition (transitions, perspective)
|
||||
"ec.transition.blockdissolve",
|
||||
"ec.transition.ccglasswipe",
|
||||
"ec.transition.ccgridwipe",
|
||||
"ec.transition.ccimagewipe",
|
||||
"ec.transition.ccjaws",
|
||||
"ec.transition.cclinesweep",
|
||||
"ec.transition.ccradialscalewipe",
|
||||
"ec.transition.ccscalewipe",
|
||||
"ec.transition.cctwister",
|
||||
"ec.transition.ccwarpomatic",
|
||||
"ec.perspective.radialshadow",
|
||||
"ec.distort.ccbender",
|
||||
"ec.distort.ccblobbylize",
|
||||
"ec.perspective.cccylinder",
|
||||
"ec.perspective.ccsphere",
|
||||
"ec.perspective.ccspotlight",
|
||||
"ec.perspective.ccenvironment",
|
||||
"ec.perspective.3dglasses",
|
||||
// effectcraft-gpu fx_text (glyph coverage rasterised on the CPU)
|
||||
"ec.text.numbers",
|
||||
"ec.text.timecode",
|
||||
// effectcraft-gpu fx_time (frames fetched by the host, combined on the GPU)
|
||||
"ec.time.timedifference",
|
||||
"ec.time.timedisplacement",
|
||||
"ec.time.ccforcemotionblur",
|
||||
"ec.time.ccwidetime",
|
||||
"ec.time.pixelmotionblur",
|
||||
"ec.time.timewarp",
|
||||
];
|
||||
|
||||
/// Effects whose output depends on [`EffectCtx::time`] directly (not only through animated
|
||||
|
||||
@@ -90,8 +90,10 @@ fn cc_environment(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let ex = u.rem_euclid(1.0) * esz[0] * es + eo[0];
|
||||
let ey = v.clamp(0.0, 1.0) * esz[1] * es + eo[1];
|
||||
let e = env_img.sample_bilinear_clamped(ex, ey);
|
||||
let (ec, _) = unpremul(e);
|
||||
*px = premul(ec, a);
|
||||
// Straight colour, with nearly transparent texels fading to black continuously
|
||||
// (a hard 1e-6 cut-off would turn the float noise of the filtered map into colour).
|
||||
let k = 1.0 / e[3].max(1e-3);
|
||||
*px = premul([e[0] * k, e[1] * k, e[2] * k], a);
|
||||
}
|
||||
});
|
||||
b
|
||||
|
||||
+122
-46
@@ -192,9 +192,9 @@ fn tint(c: [f32; 4], a: f32) -> Px {
|
||||
[c[0] * a, c[1] * a, c[2] * a, a]
|
||||
}
|
||||
|
||||
/// Draw text polylines with the given look (`r` = glyph stroke radius in px).
|
||||
fn draw_text(b: &mut Buf, polys: &[Vec<[f64; 2]>], r: f64, look: &TextLook) {
|
||||
let (w, h) = (b.img.width as usize, b.img.height as usize);
|
||||
/// Coverage of text polylines (`r` = glyph stroke radius in px): the fill and, when the look
|
||||
/// strokes, the stroke ring.
|
||||
fn text_coverage(w: usize, h: usize, polys: &[Vec<[f64; 2]>], r: f64, look: &TextLook) -> (Plane, Option<Plane>) {
|
||||
let segs_at = |rad: f64| {
|
||||
let mut s = Vec::new();
|
||||
for pl in polys {
|
||||
@@ -211,24 +211,35 @@ fn draw_text(b: &mut Buf, polys: &[Vec<[f64; 2]>], r: f64, look: &TextLook) {
|
||||
let inner = if r - sw * 0.5 > 0.05 { raster_segs(w, h, &segs_at(r - sw * 0.5), 1.0) } else { Plane::new(w, h) };
|
||||
Some(outer.zip_map(&inner, |a, b| (a - b).max(0.0)))
|
||||
};
|
||||
(fill, ring)
|
||||
}
|
||||
|
||||
/// One pixel of drawn text: `px` under the fill / stroke coverages `f`, `s` (opacity applied).
|
||||
fn text_px(px: Px, f: f32, s: f32, look: &TextLook) -> Px {
|
||||
let mut out = if look.on_original { px } else { [0.0; 4] };
|
||||
match look.display {
|
||||
1 => out = over(out, tint(look.stroke, s)),
|
||||
2 => {
|
||||
out = over(out, tint(look.stroke, s));
|
||||
out = over(out, tint(look.fill, f));
|
||||
}
|
||||
3 => {
|
||||
out = over(out, tint(look.fill, f));
|
||||
out = over(out, tint(look.stroke, s));
|
||||
}
|
||||
_ => out = over(out, tint(look.fill, f)),
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Draw text polylines with the given look (`r` = glyph stroke radius in px).
|
||||
fn draw_text(b: &mut Buf, polys: &[Vec<[f64; 2]>], r: f64, look: &TextLook) {
|
||||
let (fill, ring) = text_coverage(b.img.width as usize, b.img.height as usize, polys, r, look);
|
||||
let op = look.opacity;
|
||||
b.img.data.par_iter_mut().enumerate().for_each(|(i, px)| {
|
||||
let mut out = if look.on_original { *px } else { [0.0; 4] };
|
||||
let f = fill.data[i] * op;
|
||||
let s = ring.as_ref().map(|r| r.data[i]).unwrap_or(0.0) * op;
|
||||
match look.display {
|
||||
1 => out = over(out, tint(look.stroke, s)),
|
||||
2 => {
|
||||
out = over(out, tint(look.stroke, s));
|
||||
out = over(out, tint(look.fill, f));
|
||||
}
|
||||
3 => {
|
||||
out = over(out, tint(look.fill, f));
|
||||
out = over(out, tint(look.stroke, s));
|
||||
}
|
||||
_ => out = over(out, tint(look.fill, f)),
|
||||
}
|
||||
*px = out;
|
||||
*px = text_px(*px, f, s, look);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -337,7 +348,18 @@ fn numbers_text(ctx: &EffectCtx) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
fn numbers(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
/// What Numbers or Timecode draws: glyph polylines, stroke radius and look, whether the layer
|
||||
/// stays under the text, and Timecode's box (`[x0, x1, y0, y1]` in buffer pixels, colour,
|
||||
/// opacity × colour alpha).
|
||||
struct TextPlan {
|
||||
polys: Vec<Vec<[f64; 2]>>,
|
||||
r: f64,
|
||||
look: TextLook,
|
||||
keep: bool,
|
||||
boxed: Option<([f64; 4], [f32; 4], f32)>,
|
||||
}
|
||||
|
||||
fn numbers_plan(ctx: &EffectCtx, b: &Buf) -> TextPlan {
|
||||
let flat = ungroup(ctx.params);
|
||||
let ctx = &EffectCtx { params: &flat, time: ctx.time, layer_size: ctx.layer_size, seed: ctx.seed, adjustment: ctx.adjustment, env: ctx.env };
|
||||
let pr = ctx.params;
|
||||
@@ -349,11 +371,81 @@ fn numbers(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let wdt = line_width(&text, size, tracking, prop);
|
||||
let mut polys = Vec::new();
|
||||
layout_line(&text, [pos.0 - wdt * 0.5, pos.1 + unit(size) * 3.0], size, tracking, prop, &mut polys);
|
||||
let look = look_from(ctx, &b, "compositeOnOriginal");
|
||||
draw_text(&mut b, &polys, weight(size), &look);
|
||||
let mut look = look_from(ctx, b, "compositeOnOriginal");
|
||||
let keep = look.on_original;
|
||||
look.on_original = true;
|
||||
TextPlan { polys, r: weight(size), look, keep, boxed: None }
|
||||
}
|
||||
|
||||
/// Timecode's box over pixel (`x`, `y`).
|
||||
fn box_px(px: Px, x: usize, y: usize, (r, bc, a): ([f64; 4], [f32; 4], f32)) -> Px {
|
||||
let [x0, x1, y0, y1] = r;
|
||||
let fy = y as f64 + 0.5;
|
||||
let cy = ((fy - y0 + 0.5).clamp(0.0, 1.0) * (y1 - fy + 0.5).clamp(0.0, 1.0)) as f32;
|
||||
if cy <= 0.0 {
|
||||
return px;
|
||||
}
|
||||
let fx = x as f64 + 0.5;
|
||||
let cx = ((fx - x0 + 0.5).clamp(0.0, 1.0) * (x1 - fx + 0.5).clamp(0.0, 1.0)) as f32;
|
||||
let k = cx * cy * a;
|
||||
if k > 0.0 { over(px, [bc[0] * k, bc[1] * k, bc[2] * k, k]) } else { px }
|
||||
}
|
||||
|
||||
fn draw_plan(mut b: Buf, plan: &TextPlan) -> Buf {
|
||||
let w = b.img.width as usize;
|
||||
let (fill, ring) = text_coverage(w, b.img.height as usize, &plan.polys, plan.r, &plan.look);
|
||||
let op = plan.look.opacity;
|
||||
b.img.data.par_iter_mut().enumerate().for_each(|(i, px)| {
|
||||
let mut o = if plan.keep { *px } else { [0.0; 4] };
|
||||
if let Some(bx) = plan.boxed {
|
||||
o = box_px(o, i % w.max(1), i / w.max(1), bx);
|
||||
}
|
||||
let f = fill.data[i] * op;
|
||||
let s = ring.as_ref().map(|r| r.data[i]).unwrap_or(0.0) * op;
|
||||
*px = text_px(o, f, s, &plan.look);
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
fn numbers(ctx: &EffectCtx, b: Buf) -> Buf {
|
||||
let plan = numbers_plan(ctx, &b);
|
||||
draw_plan(b, &plan)
|
||||
}
|
||||
|
||||
/// Numbers and Timecode for the GPU compositor (effectcraft-gpu `fx_text`): the glyph
|
||||
/// coverage is rasterised here (the fill in x, the stroke ring in y, opacity applied), the
|
||||
/// kernel composites it.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TextLayer {
|
||||
pub coverage: Image,
|
||||
/// Display Options (0 fill only, 1 stroke only, 2 fill over stroke, 3 stroke over fill).
|
||||
pub display: u32,
|
||||
pub fill: [f32; 4],
|
||||
pub stroke: [f32; 4],
|
||||
/// The layer stays under the text (else the text is drawn on transparency).
|
||||
pub keep: bool,
|
||||
/// Timecode's box: `[x0, x1, y0, y1]` (buffer pixels), colour, opacity × colour alpha.
|
||||
pub boxed: Option<([f64; 4], [f32; 4], f32)>,
|
||||
}
|
||||
|
||||
/// [`TextLayer`] of Numbers (`ec.text.numbers`) or Timecode (`ec.text.timecode`) on a buffer
|
||||
/// of `b`'s geometry.
|
||||
pub fn text_layer(id: &str, ctx: &EffectCtx, b: &Buf) -> Option<TextLayer> {
|
||||
let plan = match id {
|
||||
"ec.text.numbers" => numbers_plan(ctx, b),
|
||||
"ec.text.timecode" => timecode_plan(ctx, b),
|
||||
_ => return None,
|
||||
};
|
||||
let (w, h) = (b.img.width as usize, b.img.height as usize);
|
||||
let (fill, ring) = text_coverage(w, h, &plan.polys, plan.r, &plan.look);
|
||||
let op = plan.look.opacity;
|
||||
let mut coverage = Image::new(b.img.width, b.img.height);
|
||||
coverage.data.par_iter_mut().enumerate().for_each(|(i, px)| {
|
||||
*px = [fill.data[i] * op, ring.as_ref().map(|r| r.data[i]).unwrap_or(0.0) * op, 0.0, 0.0];
|
||||
});
|
||||
Some(TextLayer { coverage, display: plan.look.display, fill: plan.look.fill, stroke: plan.look.stroke, keep: plan.keep, boxed: plan.boxed })
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Timecode
|
||||
|
||||
/// SMPTE drop-frame label for nominal 30/60 fps rates.
|
||||
@@ -368,7 +460,7 @@ fn drop_frame(frames: i64, nominal: i64) -> String {
|
||||
format!("{:02};{:02};{:02};{:02}", adj / (nominal * 3600), (adj / (nominal * 60)) % 60, (adj / nominal) % 60, adj % nominal)
|
||||
}
|
||||
|
||||
fn timecode_fx(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
fn timecode_plan(ctx: &EffectCtx, b: &Buf) -> TextPlan {
|
||||
let pr = ctx.params;
|
||||
let src = pr.e("timeSource");
|
||||
let (t, fps) = match src {
|
||||
@@ -397,36 +489,20 @@ fn timecode_fx(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let pos = b.to_px(pr.v2("textPosition"));
|
||||
let wdt = line_width(&text, size, 0.0, false);
|
||||
let op = (pr.f("opacity") / 100.0) as f32;
|
||||
if !pr.b("renderOnOriginal") {
|
||||
b.img = Image::new(b.img.width, b.img.height);
|
||||
}
|
||||
if pr.b("showBox") {
|
||||
let boxed = pr.b("showBox").then(|| {
|
||||
let bc = pr.color("boxColor");
|
||||
let pad = size * 0.25;
|
||||
let (x0, x1) = (pos.0 - wdt * 0.5 - pad, pos.0 + wdt * 0.5 + pad);
|
||||
let (y0, y1) = (pos.1 - size * 0.5, pos.1 + size * 0.5);
|
||||
let a = op * bc[3];
|
||||
b.img.rows_mut().for_each(|(y, row)| {
|
||||
let fy = y as f64 + 0.5;
|
||||
let cy = ((fy - y0 + 0.5).clamp(0.0, 1.0) * (y1 - fy + 0.5).clamp(0.0, 1.0)) as f32;
|
||||
if cy <= 0.0 {
|
||||
return;
|
||||
}
|
||||
for (x, px) in row.iter_mut().enumerate() {
|
||||
let fx = x as f64 + 0.5;
|
||||
let cx = ((fx - x0 + 0.5).clamp(0.0, 1.0) * (x1 - fx + 0.5).clamp(0.0, 1.0)) as f32;
|
||||
let k = cx * cy * a;
|
||||
if k > 0.0 {
|
||||
*px = over(*px, [bc[0] * k, bc[1] * k, bc[2] * k, k]);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
([pos.0 - wdt * 0.5 - pad, pos.0 + wdt * 0.5 + pad, pos.1 - size * 0.5, pos.1 + size * 0.5], bc, op * bc[3])
|
||||
});
|
||||
let mut polys = Vec::new();
|
||||
layout_line(&text, [pos.0 - wdt * 0.5, pos.1 + unit(size) * 3.0], size, 0.0, false, &mut polys);
|
||||
let look = TextLook { display: 0, fill: pr.color("textColor"), stroke: [0.0; 4], stroke_w: 0.0, on_original: true, opacity: op };
|
||||
draw_text(&mut b, &polys, weight(size), &look);
|
||||
b
|
||||
TextPlan { polys, r: weight(size), look, keep: pr.b("renderOnOriginal"), boxed }
|
||||
}
|
||||
|
||||
fn timecode_fx(ctx: &EffectCtx, b: Buf) -> Buf {
|
||||
let plan = timecode_plan(ctx, &b);
|
||||
draw_plan(b, &plan)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Basic Text
|
||||
|
||||
+122
-19
@@ -680,23 +680,56 @@ fn fetch_layer(ctx: &EffectCtx, id: &str, times: &[f64], grid: &Buf) -> Option<V
|
||||
)
|
||||
}
|
||||
|
||||
/// Timewarp. Whole Frames shows the nearest source frame, Frame Mix cross-fades the two
|
||||
/// neighbouring frames and Pixel Motion builds the in-between frame along estimated motion
|
||||
/// vectors (block-matching optical flow, [`effectcraft_raster::flow`]) tuned by the Tuning
|
||||
/// group. A Matte Layer splits foreground and background so each moves on its own vectors; a
|
||||
/// Warp Layer supplies the motion instead of the layer itself. Motion Blur averages Shutter
|
||||
/// Samples instants across the shutter.
|
||||
fn timewarp(ctx: &EffectCtx, b: Buf) -> Buf {
|
||||
if ctx.env.host.is_none() {
|
||||
return b;
|
||||
/// One output instant of a Timewarp without a Matte Layer: a fetched frame, a cross-fade of two,
|
||||
/// or the in-between frame built along a motion field ([`TimewarpPlan::flows`]).
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum TwStep {
|
||||
Whole(usize),
|
||||
Mix(usize, usize, f32),
|
||||
Motion { a: usize, b: usize, w: f32, flow: usize },
|
||||
}
|
||||
|
||||
/// Timewarp without a Matte Layer, prepared for the GPU compositor (effectcraft-gpu `fx_time`):
|
||||
/// the fetched frames on one grid, the instants across the shutter (with their weights) and the
|
||||
/// motion fields (block matching on the CPU). [`TimewarpPlan::render_cpu`] is the CPU effect.
|
||||
pub struct TimewarpPlan {
|
||||
pub grid: Buf,
|
||||
pub frames: Vec<Image>,
|
||||
pub steps: Vec<(TwStep, f32)>,
|
||||
pub flows: Vec<Flow>,
|
||||
pub pm: PixelMotion,
|
||||
}
|
||||
|
||||
impl TimewarpPlan {
|
||||
pub fn render_cpu(&self) -> Buf {
|
||||
let f = &self.frames;
|
||||
let imgs: Vec<Image> = self
|
||||
.steps
|
||||
.iter()
|
||||
.map(|(s, _)| match *s {
|
||||
TwStep::Whole(i) => f[i].clone(),
|
||||
TwStep::Mix(i, j, w) => effectcraft_raster::flow::mix(&f[i], &f[j], w),
|
||||
TwStep::Motion { a, b, w, flow } => timewarp_interpolate(&f[a], &f[b], w, &self.flows[flow], &self.pm),
|
||||
})
|
||||
.collect();
|
||||
let weights: Vec<f32> = self.steps.iter().map(|(_, w)| *w).collect();
|
||||
let img = if imgs.len() == 1 { imgs.into_iter().next().unwrap_or_default() } else { average(&imgs, &weights) };
|
||||
with_img(self.grid.clone(), img)
|
||||
}
|
||||
}
|
||||
|
||||
/// Source-crop the edges of a frame (Pixel Motion works on cropped frames).
|
||||
pub fn timewarp_crop(img: &Image, pm: &PixelMotion) -> Image {
|
||||
crop_edges(img, pm.crops)
|
||||
}
|
||||
|
||||
/// The source instants Timewarp shows at the frame (with their weights) and the layer times
|
||||
/// they need: (instants, frame times, frames per second).
|
||||
fn timewarp_instants(ctx: &EffectCtx) -> (Vec<(f64, f32)>, Vec<f64>, f64) {
|
||||
let fps = ctx.fps();
|
||||
let by_frame = ctx.params.e("adjustTimeBy") == 1;
|
||||
let speed = ctx.params.f("speed");
|
||||
let src = timewarp_source_time(ctx.time, by_frame, speed, ctx.params.f("sourceFrame"), fps);
|
||||
let method = ctx.params.e("method");
|
||||
let one = method > 0 && ctx.params.b("tuning/buildFromOneImage");
|
||||
// Instants of source time (with weights) across the shutter.
|
||||
let mut instants: Vec<(f64, f32)> = vec![];
|
||||
if ctx.params.b("motionBlur/enableMotionBlur") {
|
||||
let angle = if ctx.params.e("motionBlur/shutterControl") == 1 { ctx.params.f("motionBlur/shutterAngle") } else { 180.0 };
|
||||
@@ -709,18 +742,88 @@ fn timewarp(ctx: &EffectCtx, b: Buf) -> Buf {
|
||||
} else {
|
||||
instants.push((src, 1.0));
|
||||
}
|
||||
// Each instant needs the frame at or before it and the next one.
|
||||
let split = |s: f64| {
|
||||
let f = s * fps;
|
||||
let f0 = (f + 1e-6).floor();
|
||||
(f0, ((f - f0) as f32).max(0.0))
|
||||
};
|
||||
let mut times: Vec<f64> = vec![];
|
||||
for &(s, _) in &instants {
|
||||
let (f0, _) = split(s);
|
||||
let (f0, _) = tw_split(s, fps);
|
||||
times.push(f0 / fps);
|
||||
times.push((f0 + 1.0) / fps);
|
||||
}
|
||||
(instants, times, fps)
|
||||
}
|
||||
|
||||
/// The frame at or before source time `s` and the fraction to the next one.
|
||||
fn tw_split(s: f64, fps: f64) -> (f64, f32) {
|
||||
let f = s * fps;
|
||||
let f0 = (f + 1e-6).floor();
|
||||
(f0, ((f - f0) as f32).max(0.0))
|
||||
}
|
||||
|
||||
/// [`TimewarpPlan`] of a Timewarp instance without a Matte Layer. `None` without the effect host
|
||||
/// (the layer passes through) or with a Matte Layer.
|
||||
pub fn timewarp_plan(ctx: &EffectCtx) -> Option<TimewarpPlan> {
|
||||
ctx.env.host?;
|
||||
if ctx.params.get("matteLayer").and_then(Value::as_layer).is_some() {
|
||||
return None;
|
||||
}
|
||||
let (instants, times, fps) = timewarp_instants(ctx);
|
||||
let method = ctx.params.e("method");
|
||||
let one = method > 0 && ctx.params.b("tuning/buildFromOneImage");
|
||||
let fr = fetch(ctx, ×)?;
|
||||
let pm = PixelMotion::from(ctx, fr.grid.scale);
|
||||
let warps = fetch_layer(ctx, "warpLayer", ×, &fr.grid);
|
||||
let idx = |t: f64| times.iter().position(|q| (q - t).abs() < 1e-9).unwrap_or(0);
|
||||
let mut flows: Vec<(i64, Flow)> = vec![];
|
||||
let mut steps = vec![];
|
||||
for &(s, wgt) in &instants {
|
||||
let (f0, frac) = tw_split(s, fps);
|
||||
let (i0, i1) = (idx(f0 / fps), idx((f0 + 1.0) / fps));
|
||||
let step = if method == 0 || frac < 1e-4 || (one && method == 1) {
|
||||
let pick = if method == 0 || frac < 1e-4 { 0.0 } else { frac.round() };
|
||||
TwStep::Whole(if pick < 0.5 { i0 } else { i1 })
|
||||
} else if method == 1 {
|
||||
TwStep::Mix(i0, i1, frac)
|
||||
} else {
|
||||
let key = f0 as i64;
|
||||
let flow = match flows.iter().position(|(k, _)| *k == key) {
|
||||
Some(k) => k,
|
||||
None => {
|
||||
let f = match &warps {
|
||||
Some(w) => timewarp_flow(&w[i0], &w[i1], &pm),
|
||||
None => timewarp_flow(&fr.imgs[i0], &fr.imgs[i1], &pm),
|
||||
};
|
||||
flows.push((key, f));
|
||||
flows.len() - 1
|
||||
}
|
||||
};
|
||||
TwStep::Motion { a: i0, b: i1, w: frac, flow }
|
||||
};
|
||||
steps.push((step, wgt));
|
||||
}
|
||||
Some(TimewarpPlan { grid: fr.grid, frames: fr.imgs, steps, flows: flows.into_iter().map(|(_, f)| f).collect(), pm })
|
||||
}
|
||||
|
||||
/// Timewarp. Whole Frames shows the nearest source frame, Frame Mix cross-fades the two
|
||||
/// neighbouring frames and Pixel Motion builds the in-between frame along estimated motion
|
||||
/// vectors (block-matching optical flow, [`effectcraft_raster::flow`]) tuned by the Tuning
|
||||
/// group. A Matte Layer splits foreground and background so each moves on its own vectors; a
|
||||
/// Warp Layer supplies the motion instead of the layer itself. Motion Blur averages Shutter
|
||||
/// Samples instants across the shutter.
|
||||
fn timewarp(ctx: &EffectCtx, b: Buf) -> Buf {
|
||||
if ctx.env.host.is_none() {
|
||||
return b;
|
||||
}
|
||||
if ctx.params.get("matteLayer").and_then(Value::as_layer).is_none() {
|
||||
return match timewarp_plan(ctx) {
|
||||
Some(plan) => plan.render_cpu(),
|
||||
None => b,
|
||||
};
|
||||
}
|
||||
// Instants of source time (with weights) across the shutter; each needs the frame at or
|
||||
// before it and the next one.
|
||||
let (instants, times, fps) = timewarp_instants(ctx);
|
||||
let split = |s: f64| tw_split(s, fps);
|
||||
let method = ctx.params.e("method");
|
||||
let one = method > 0 && ctx.params.b("tuning/buildFromOneImage");
|
||||
let Some(fr) = fetch(ctx, ×) else { return b };
|
||||
let grid = fr.grid.clone();
|
||||
let frame = |t: f64| -> &Image {
|
||||
|
||||
+36
-4
@@ -358,7 +358,7 @@ pub(crate) struct Post {
|
||||
dummy_out: wgpu::TextureView,
|
||||
}
|
||||
|
||||
const KERNELS: &[&str] = &["resolve", "radius_of", "boost", "prefix_rows", "gather", "finish", "wire"];
|
||||
const KERNELS: &[&str] = &["resolve", "radius_of", "boost", "prefix_rows", "gather", "finish", "wire", "occlude"];
|
||||
|
||||
impl Post {
|
||||
fn new(device: &wgpu::Device) -> Post {
|
||||
@@ -544,7 +544,7 @@ fn read_buffer(e: &mut Enc, buf: &wgpu::Buffer, size: u64) -> Option<Vec<u8>> {
|
||||
|
||||
/// A resolved run on the GPU: premultiplied linear (or working-space) colour and camera depth
|
||||
/// (`BIG` where nothing was drawn), one entry per output pixel.
|
||||
struct Resolved {
|
||||
pub(crate) struct Resolved {
|
||||
acc: wgpu::Buffer,
|
||||
depth: wgpu::Buffer,
|
||||
width: u32,
|
||||
@@ -557,7 +557,7 @@ const BIG: f32 = 3.0e38;
|
||||
/// Rasterise every motion-blur sub-sample of a prepared run, resolve and average them, then
|
||||
/// apply the depth of field (`render_prepared`'s steps). `None` = can't run here (sizes,
|
||||
/// limits, or a depth of field without readback); `Some(None)` = nothing drawn.
|
||||
fn resolve_run(e: &mut Enc, prep: &Prepared) -> Option<Option<Resolved>> {
|
||||
pub(crate) fn resolve_run(e: &mut Enc, prep: &Prepared) -> Option<Option<Resolved>> {
|
||||
let (w, h) = prep.out;
|
||||
let g = e.g;
|
||||
if !g.fits(w, h) || (prep.dof.is_some() && !g.can_wait()) {
|
||||
@@ -661,7 +661,7 @@ fn depth_of_field(e: &mut Enc, acc: &wgpu::Buffer, depth: &wgpu::Buffer, (w, h):
|
||||
}
|
||||
|
||||
/// The resolved run as an image in the canvas's encoding, composited over `canvas` when given.
|
||||
fn finish(e: &mut Enc, r: &Resolved, encode: bool, canvas: Option<&GpuImage>) -> GpuImage {
|
||||
pub(crate) fn finish(e: &mut Enc, r: &Resolved, encode: bool, canvas: Option<&GpuImage>) -> GpuImage {
|
||||
let out = e.image(r.width, r.height);
|
||||
let ov = out.texture.create_view(&Default::default());
|
||||
let cv = canvas.map(|c| c.texture.create_view(&Default::default()));
|
||||
@@ -704,6 +704,38 @@ pub(crate) fn draw_run(e: &mut Enc, prep: &Prepared, canvas: &GpuImage) -> Optio
|
||||
}
|
||||
}
|
||||
|
||||
/// A layer rendered on its own (`iso`), finished, and hidden where the run's main scene is
|
||||
/// nearer (`adv::draw_run`'s 2D compositing path).
|
||||
pub(crate) fn occluded(e: &mut Enc, iso: &Resolved, main: &Resolved, encode: bool) -> GpuImage {
|
||||
let img = finish(e, iso, encode, None);
|
||||
let (w, h) = (iso.width, iso.height);
|
||||
let out = e.image(w, h);
|
||||
let (iv, ov) = (img.texture.create_view(&Default::default()), out.texture.create_view(&Default::default()));
|
||||
let b =
|
||||
Binds { color: Some(&iv), bufs: [None, Some(&iso.depth), Some(&main.acc), None, Some(&main.depth), None, None], out: Some(&ov), ..Default::default() };
|
||||
dispatch(e, "occlude", [w, h, 0, 0], [0.0; 4], b, pixel_groups(w, h));
|
||||
out
|
||||
}
|
||||
|
||||
/// Kernels of `sky.wgsl` (the shared compute module).
|
||||
pub(crate) const SKY_KERNELS: &[&str] = &["adv_sky"];
|
||||
|
||||
/// An Environment Light Background layer drawn over `canvas` (`three_d::compose::draw_sky`).
|
||||
pub(crate) fn sky(e: &mut Enc, sky: &effectcraft_render::three_d::SkyDraw, canvas: &GpuImage) -> Option<GpuImage> {
|
||||
let img = e.g.upload_buf(&sky.buf)?;
|
||||
let f3 = |v: [f64; 3], w: f64| [v[0] as f32, v[1] as f32, v[2] as f32, w as f32];
|
||||
let mut p = crate::context::Params::default();
|
||||
p.f[0] = f3(sky.fwd, sky.zoom);
|
||||
p.f[1] = f3(sky.right, sky.ortho as u32 as f64);
|
||||
p.f[2] = [sky.down[0] as f32, sky.down[1] as f32, sky.down[2] as f32, sky.rotation];
|
||||
let s = sky.scale;
|
||||
p.f[3] = [(sky.roi.0 / s - sky.view.0 / 2.0) as f32, (sky.roi.1 / s - sky.view.1 / 2.0) as f32, (1.0 / s) as f32, sky.opacity];
|
||||
p.f[4] = [img.width as f32, img.height as f32, 0.0, 0.0];
|
||||
let out = e.scratch(canvas.width, canvas.height);
|
||||
e.pixels("adv_sky", &p, canvas, Some(&img), &out, None);
|
||||
Some(out)
|
||||
}
|
||||
|
||||
/// Wireframe-quality layer outlines: `canvas` with `pixels` set to opaque white
|
||||
/// ([`effectcraft_render::Renderer::wireframe_pixels`]).
|
||||
pub(crate) fn wireframe(e: &mut Enc, canvas: &GpuImage, pixels: &[(u32, u32)]) -> GpuImage {
|
||||
|
||||
@@ -316,6 +316,11 @@ impl GpuContext {
|
||||
include_str!("shaders/fx_tone.wgsl"),
|
||||
include_str!("shaders/fx_warp.wgsl"),
|
||||
include_str!("shaders/fx_extra.wgsl"),
|
||||
include_str!("shaders/fx_light.wgsl"),
|
||||
include_str!("shaders/fx_transition.wgsl"),
|
||||
include_str!("shaders/fx_text.wgsl"),
|
||||
include_str!("shaders/fx_time.wgsl"),
|
||||
include_str!("shaders/sky.wgsl"),
|
||||
]
|
||||
.concat();
|
||||
let module =
|
||||
@@ -375,6 +380,11 @@ impl GpuContext {
|
||||
.chain(crate::fx_tone::KERNELS)
|
||||
.chain(crate::fx_warp::KERNELS)
|
||||
.chain(crate::fx_extra::KERNELS)
|
||||
.chain(crate::fx_light::KERNELS)
|
||||
.chain(crate::fx_transition::KERNELS)
|
||||
.chain(crate::fx_text::KERNELS)
|
||||
.chain(crate::fx_time::KERNELS)
|
||||
.chain(crate::adv3d::SKY_KERNELS)
|
||||
.map(|e| (e, &layout))
|
||||
.chain(EXT_ENTRIES.iter().map(|e| (e, &layout_ext)))
|
||||
.map(|(e, layout)| {
|
||||
|
||||
@@ -167,6 +167,10 @@ fn apply(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
_ if crate::fx_tone::IDS.contains(&id) => crate::fx_tone::apply(e, id, ctx, b),
|
||||
_ if crate::fx_warp::IDS.contains(&id) => crate::fx_warp::apply(e, id, ctx, b),
|
||||
_ if crate::fx_extra::IDS.contains(&id) => crate::fx_extra::apply(e, id, ctx, b),
|
||||
_ if crate::fx_light::IDS.contains(&id) => crate::fx_light::apply(e, id, ctx, b),
|
||||
_ 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),
|
||||
_ => pointwise(e, id, ctx, b),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
//! GPU effects, the CC light family (kernels in `shaders/fx_light.wgsl`, every entry point
|
||||
//! prefixed `flt_`): CC Light Rays, CC Light Burst 2.5, CC Light Sweep and CC Light Wipe, with
|
||||
//! the CPU effects' exact steps. Centres, radii, the wipe's corner distance and ramp are
|
||||
//! computed on the CPU in f64 as the CPU effect does; the kernels work relative to the centre.
|
||||
//! Light Sweep's softened alpha plane is the CPU's `gauss_plane` (box passes, edges repeated).
|
||||
|
||||
use effectcraft_effects::EffectCtx;
|
||||
|
||||
use crate::context::{Enc, GpuImage, Params};
|
||||
use crate::effects::{GBuf, gaussian_blur};
|
||||
|
||||
/// Compute entry points in `fx_light.wgsl`.
|
||||
pub(crate) const KERNELS: &[&str] = &["flt_rays", "flt_burst", "flt_sweep", "flt_wipe"];
|
||||
|
||||
/// Effect ids implemented here.
|
||||
pub(crate) const IDS: &[&str] = &["ec.generate.cclightrays", "ec.generate.cclightburst", "ec.generate.cclightsweep", "ec.transition.cclightwipe"];
|
||||
|
||||
/// 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.generate.cclightrays" => light_rays(e, ctx, b),
|
||||
"ec.generate.cclightburst" => light_burst(e, ctx, b),
|
||||
"ec.generate.cclightsweep" => light_sweep(e, ctx, b),
|
||||
"ec.transition.cclightwipe" => light_wipe(e, 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>) -> Option<GBuf> {
|
||||
let out = e.scratch(b.img.width, b.img.height);
|
||||
e.pixels(entry, p, &b.img, aux, &out, None);
|
||||
b.img = out;
|
||||
Some(b)
|
||||
}
|
||||
|
||||
fn rgb(c: [f32; 4]) -> [f32; 4] {
|
||||
[c[0], c[1], c[2], 0.0]
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Light Rays / Burst (generate2.rs)
|
||||
|
||||
fn light_rays(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let pr = ctx.params;
|
||||
let c = b.to_px(pr.v2("center"));
|
||||
let k = (pr.f("intensity") / 100.0 * 0.5).clamp(0.0, 0.95);
|
||||
let gain = (pr.f("intensity") / 100.0 * 2.0) as f32;
|
||||
let rad = (pr.f("radius") * b.scale).max(0.5);
|
||||
let ws = (pr.f("warpSoftness") / 50.0).max(0.0);
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [(pr.e("shape") == 1) as u32, pr.b("colorFromSource") as u32, pr.e("transferMode"), 0];
|
||||
p.f[0] = [c.0 as f32, c.1 as f32, k as f32, gain];
|
||||
p.f[1] = [rad as f32, (rad * (1.0 + ws)) as f32 + 1e-3, 0.0, 0.0];
|
||||
p.f[2] = rgb(pr.color("color"));
|
||||
run(e, "flt_rays", &p, b, None)
|
||||
}
|
||||
|
||||
fn light_burst(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let pr = ctx.params;
|
||||
let c = b.to_px(pr.v2("center"));
|
||||
let inten = (pr.f("intensity") / 100.0) as f32;
|
||||
let k = (pr.f("rayLength") / 100.0).clamp(0.0, 1.0);
|
||||
let set_color = pr.b("setColor");
|
||||
if k <= 0.0 && (inten - 1.0).abs() < 1e-6 && !set_color {
|
||||
return Some(b);
|
||||
}
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [pr.e("burst"), set_color as u32, 0, 0];
|
||||
p.f[0] = [c.0 as f32, c.1 as f32, k as f32, inten];
|
||||
p.f[1] = rgb(pr.color("color"));
|
||||
run(e, "flt_burst", &p, b, None)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Light Sweep (generate2.rs)
|
||||
|
||||
fn light_sweep(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let pr = ctx.params;
|
||||
let c = b.to_px(pr.v2("center"));
|
||||
let dir = pr.f("direction").to_radians();
|
||||
let half = (pr.f("width") * b.scale * 0.5).max(0.5);
|
||||
let edge_t = pr.f("edgeThickness") * b.scale;
|
||||
// The alpha plane (x), softened by util::gauss_plane (edges repeated) when thick enough.
|
||||
let alpha = e.scratch(b.img.width, b.img.height);
|
||||
e.pixels("fxe_alpha", &Params::default(), &b.img, None, &alpha, None);
|
||||
let soft = if edge_t > 0.05 { gaussian_blur(e, &alpha, edge_t * 0.5, edge_t * 0.5, true) } else { alpha };
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [pr.e("shape"), pr.e("lightReceptionMode"), 0, 0];
|
||||
p.f[0] = [c.0 as f32, c.1 as f32, dir.cos() as f32, dir.sin() as f32];
|
||||
p.f[1] = [half as f32, (pr.f("sweepIntensity") / 100.0) as f32, (pr.f("edgeIntensity") / 100.0) as f32, 0.0];
|
||||
p.f[2] = rgb(pr.color("lightColor"));
|
||||
run(e, "flt_sweep", &p, b, Some(&soft))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Light Wipe (transition.rs)
|
||||
|
||||
fn light_wipe(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let pr = ctx.params;
|
||||
let done = (pr.f("completion") / 100.0).clamp(0.0, 1.0);
|
||||
if done <= 0.0 {
|
||||
return Some(b);
|
||||
}
|
||||
let c = b.to_px(pr.v2("center"));
|
||||
let shape = pr.e("shape");
|
||||
let (s, co) = pr.f("direction").to_radians().sin_cos();
|
||||
let reverse = pr.b("reverse");
|
||||
let metric = |dx: f64, dy: f64| {
|
||||
let (qx, qy) = (dx * co + dy * s, -dx * s + dy * co);
|
||||
match shape {
|
||||
0 => qx.abs(),
|
||||
2 => qx.abs().max(qy.abs()),
|
||||
_ => qx.hypot(qy),
|
||||
}
|
||||
};
|
||||
// transition::max_corner_dist.
|
||||
let (w, h) = (b.img.width as f64, b.img.height as f64);
|
||||
let maxd = [(0.0, 0.0), (w, 0.0), (0.0, h), (w, h)].iter().map(|(x, y)| metric(x - c.0, y - c.1)).fold(1e-6, f64::max);
|
||||
let edge = if reverse { (1.0 - done) * maxd } else { done * maxd };
|
||||
let band = (maxd * 0.06).max(2.0);
|
||||
let ramp = (done * 20.0).min(1.0) as f32 * ((1.0 - done) * 20.0).min(1.0) as f32;
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [shape, pr.b("colorFromSource") as u32, reverse as u32, 0];
|
||||
p.f[0] = [c.0 as f32, c.1 as f32, s as f32, co as f32];
|
||||
p.f[1] = [edge as f32, band as f32, pr.f("intensity") as f32 / 100.0 * ramp, 0.0];
|
||||
p.f[2] = rgb(pr.color("color"));
|
||||
run(e, "flt_wipe", &p, b, None)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
//! GPU effects, Numbers and Timecode (kernel `ftx_text` in `shaders/fx_text.wgsl`). The text
|
||||
//! is laid out and its glyph strokes rasterised on the CPU (`effectcraft_effects::text_layer`,
|
||||
//! the CPU effect's own coverage: fill and stroke ring), uploaded as one image and composited
|
||||
//! on the GPU with the layer, Timecode's box and the fill / stroke colours.
|
||||
|
||||
use effectcraft_effects::{Buf, EffectCtx};
|
||||
use effectcraft_raster::Image;
|
||||
|
||||
use crate::context::{Enc, Params};
|
||||
use crate::effects::GBuf;
|
||||
|
||||
/// Compute entry points in `fx_text.wgsl`.
|
||||
pub(crate) const KERNELS: &[&str] = &["ftx_text"];
|
||||
|
||||
/// Effect ids implemented here.
|
||||
pub(crate) const IDS: &[&str] = &["ec.text.numbers", "ec.text.timecode"];
|
||||
|
||||
/// 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> {
|
||||
let shape = Buf { img: Image { width: b.img.width, height: b.img.height, data: vec![] }, offset: b.offset, scale: b.scale };
|
||||
let t = effectcraft_effects::text_layer(id, ctx, &shape)?;
|
||||
let cov = e.g.upload_image(&t.coverage)?;
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [t.display, t.keep as u32, t.boxed.is_some() as u32, 0];
|
||||
p.f[0] = t.fill;
|
||||
p.f[1] = t.stroke;
|
||||
if let Some((r, c, a)) = t.boxed {
|
||||
p.f[2] = r.map(|v| v as f32);
|
||||
p.f[3] = [c[0], c[1], c[2], a];
|
||||
}
|
||||
let out = e.scratch(b.img.width, b.img.height);
|
||||
e.pixels("ftx_text", &p, &b.img, Some(&cov), &out, None);
|
||||
Some(GBuf { img: out, ..b })
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
//! GPU effects, time (kernels in `shaders/fx_time.wgsl`, every entry point prefixed `ftm_`):
|
||||
//! Time Difference, Time Displacement, CC Force Motion Blur, CC Wide Time, Pixel Motion Blur and
|
||||
//! Timewarp. Like Echo (`fx_noise`), the other frames come from the effect host on the CPU
|
||||
//! (`effectcraft_effects::time_frames`: fetched at the CPU effect's layer times and placed on
|
||||
//! one grid) and are uploaded; the kernels combine them with the CPU effect's arithmetic in its
|
||||
//! order (weighted sums one frame per pass, so the float rounding matches).
|
||||
//!
|
||||
//! Time Displacement's per-pixel time (the map's luminance, quantised to the time resolution)
|
||||
//! is computed on the CPU, since it decides which frames to fetch; the GPU gathers each pixel
|
||||
//! from its frame. Timewarp's motion vectors (block matching and smoothing,
|
||||
//! `effectcraft_raster::flow`) are estimated on the CPU (`effectcraft_effects::timewarp_plan`);
|
||||
//! the frame building (Whole Frames, Frame Mix, Pixel Motion's warp along the vectors with its
|
||||
//! error fallback, the shutter samples' average) runs on the GPU. Timewarp with a Matte Layer
|
||||
//! renders on the CPU (`catalog::gpu_supported`).
|
||||
|
||||
use effectcraft_effects::util::{fit_layer, unpremul};
|
||||
use effectcraft_effects::{Buf, EffectCtx, TwStep};
|
||||
use effectcraft_keyframe::Value;
|
||||
|
||||
use crate::context::{Enc, GpuImage, Params};
|
||||
use crate::effects::GBuf;
|
||||
|
||||
/// Compute entry points in `fx_time.wgsl`.
|
||||
pub(crate) const KERNELS: &[&str] = &["ftm_acc", "ftm_mix", "ftm_difference", "ftm_displace", "ftm_motion"];
|
||||
|
||||
/// Effect ids implemented here.
|
||||
pub(crate) const IDS: &[&str] =
|
||||
&["ec.time.timedifference", "ec.time.timedisplacement", "ec.time.ccforcemotionblur", "ec.time.ccwidetime", "ec.time.pixelmotionblur", "ec.time.timewarp"];
|
||||
|
||||
/// 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.time.timedifference" => time_difference(e, ctx, b),
|
||||
"ec.time.timedisplacement" => time_displacement(e, ctx, b),
|
||||
"ec.time.ccforcemotionblur" => {
|
||||
let n = ctx.params.f("motionBlurLevels").round().clamp(2.0, 64.0) as usize;
|
||||
let angle = if ctx.params.b("overrideShutterAngle") { ctx.params.f("shutterAngle") } else { 180.0 };
|
||||
let span = angle.clamp(0.0, 3600.0) / 360.0 / ctx.fps();
|
||||
mean_of(e, ctx, b, &(0..n).map(|i| ctx.time + span * i as f64 / n as f64).collect::<Vec<_>>())
|
||||
}
|
||||
"ec.time.ccwidetime" => {
|
||||
let fwd = ctx.params.f("forwardSteps").round().clamp(0.0, 45.0) as i64;
|
||||
let back = ctx.params.f("backwardSteps").round().clamp(0.0, 45.0) as i64;
|
||||
let fd = 1.0 / ctx.fps();
|
||||
mean_of(e, ctx, b, &(-back..=fwd).map(|k| ctx.time + k as f64 * fd).collect::<Vec<_>>())
|
||||
}
|
||||
"ec.time.pixelmotionblur" => {
|
||||
let angle = if ctx.params.e("shutterControl") == 1 { ctx.params.f("shutterAngle") } else { 180.0 };
|
||||
let n = ctx.params.f("shutterSamples").round().clamp(1.0, 64.0) as usize;
|
||||
let span = angle.clamp(0.0, 720.0) / 360.0 / ctx.fps();
|
||||
let times: Vec<f64> = (0..n).map(|i| ctx.time - span * 0.5 + if n > 1 { span * i as f64 / (n - 1) as f64 } else { span * 0.5 }).collect();
|
||||
mean_of(e, ctx, b, ×)
|
||||
}
|
||||
"ec.time.timewarp" => timewarp(e, ctx, b),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn kernel(e: &mut Enc, entry: &str, p: &Params, src: &GpuImage, aux: Option<&GpuImage>, data: Option<&wgpu::Buffer>) -> GpuImage {
|
||||
let out = e.scratch(src.width, src.height);
|
||||
e.pixels(entry, p, src, aux, &out, data);
|
||||
out
|
||||
}
|
||||
|
||||
/// time_fx::average: Σ frame × weight, one frame per pass in the CPU's order.
|
||||
fn average(e: &mut Enc, frames: &[GpuImage], weights: &[f32]) -> Option<GpuImage> {
|
||||
let first = frames.first()?;
|
||||
let mut acc = e.zeros(first.width, first.height);
|
||||
for (f, &k) in frames.iter().zip(weights) {
|
||||
let mut p = Params::default();
|
||||
p.f[0][0] = k;
|
||||
acc = kernel(e, "ftm_acc", &p, &acc, Some(f), None);
|
||||
}
|
||||
Some(acc)
|
||||
}
|
||||
|
||||
fn on_grid(img: GpuImage, grid: &Buf) -> GBuf {
|
||||
GBuf { img, offset: grid.offset, scale: grid.scale }
|
||||
}
|
||||
|
||||
/// time_fx::mean_of: the plain average of the layer at `times`.
|
||||
fn mean_of(e: &mut Enc, ctx: &EffectCtx, b: GBuf, times: &[f64]) -> Option<GBuf> {
|
||||
if times.is_empty() {
|
||||
return Some(b);
|
||||
}
|
||||
let Some((grid, imgs)) = effectcraft_effects::time_frames(ctx, times) else { return Some(b) };
|
||||
let frames = imgs.iter().map(|i| e.g.upload_image(i)).collect::<Option<Vec<_>>>()?;
|
||||
let k = 1.0 / times.len() as f32;
|
||||
let img = average(e, &frames, &vec![k; times.len()])?;
|
||||
Some(on_grid(img, &grid))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Time Difference
|
||||
|
||||
fn time_difference(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let Some(host) = ctx.env.host else { return Some(b) };
|
||||
let pr = ctx.params;
|
||||
let off = pr.f("timeOffset");
|
||||
let target_id = pr.get("targetLayer").and_then(Value::as_layer);
|
||||
let (grid, cur, target) = match target_id.and_then(|id| host.layer_at(id, ctx.env.comp_time + off, false)) {
|
||||
Some(other) => {
|
||||
let Some((grid, mut imgs)) = effectcraft_effects::time_frames(ctx, &[ctx.time]) else { return Some(b) };
|
||||
let cur = imgs.pop().unwrap_or_default();
|
||||
let tgt = fit_layer(ctx, &Buf { img: cur.clone(), ..grid.clone() }, &other, true);
|
||||
(grid, cur, tgt)
|
||||
}
|
||||
None => {
|
||||
let Some((grid, mut imgs)) = effectcraft_effects::time_frames(ctx, &[ctx.time, ctx.time + off]) else { return Some(b) };
|
||||
let tgt = imgs.pop().unwrap_or_default();
|
||||
let cur = imgs.pop().unwrap_or_default();
|
||||
(grid, cur, tgt)
|
||||
}
|
||||
};
|
||||
let (cur, target) = (e.g.upload_image(&cur)?, e.g.upload_image(&target)?);
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [pr.b("absoluteDifference") as u32, pr.e("alphaChannel"), 0, 0];
|
||||
p.f[0][0] = 1.0 + pr.f("contrast") as f32 / 25.0;
|
||||
let img = kernel(e, "ftm_difference", &p, &cur, Some(&target), None);
|
||||
Some(on_grid(img, &grid))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Time Displacement
|
||||
|
||||
fn time_displacement(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
if ctx.env.host.is_none() {
|
||||
return Some(b);
|
||||
}
|
||||
let pr = ctx.params;
|
||||
let max = pr.f("maxDisplacementTime");
|
||||
let res = pr.f("timeResolution").clamp(1.0, 999.0);
|
||||
let Some((grid, mut imgs)) = effectcraft_effects::time_frames(ctx, &[ctx.time]) else { return Some(b) };
|
||||
let grid = Buf { img: imgs.pop().unwrap_or_default(), ..grid };
|
||||
let map = match ctx.layer_param("displacementMapLayer", true) {
|
||||
Some(o) => fit_layer(ctx, &grid, &o, pr.b("stretchMap")),
|
||||
None => grid.img.clone(),
|
||||
};
|
||||
// The CPU effect's per-pixel time step (luminance → −max..+max, quantised).
|
||||
let span = 2.0 * max.abs() * res;
|
||||
let step = if span > 63.0 { 2.0 * max.abs() / 63.0 } else { 1.0 / res };
|
||||
let idx: Vec<i32> = map
|
||||
.data
|
||||
.iter()
|
||||
.map(|p| {
|
||||
let (c, a) = unpremul(*p);
|
||||
let l = if a > 0.0 { effectcraft_color::luminance(c[0], c[1], c[2]) } else { 0.5 };
|
||||
(((l as f64 - 0.5) * 2.0 * max) / step).round() as i32
|
||||
})
|
||||
.collect();
|
||||
let mut keys = idx.clone();
|
||||
keys.sort_unstable();
|
||||
keys.dedup();
|
||||
if keys == [0] {
|
||||
return Some(on_grid(e.g.upload_image(&grid.img)?, &grid));
|
||||
}
|
||||
let times: Vec<f64> = keys.iter().map(|k| ctx.time + *k as f64 * step).collect();
|
||||
let Some((fgrid, frames)) = effectcraft_effects::time_frames(ctx, ×) else { return Some(on_grid(e.g.upload_image(&grid.img)?, &grid)) };
|
||||
let dx = (fgrid.offset[0] - grid.offset[0]).round() as i32;
|
||||
let dy = (fgrid.offset[1] - grid.offset[1]).round() as i32;
|
||||
let which: Vec<u8> = idx.iter().flat_map(|k| (keys.binary_search(k).unwrap_or(0) as u32).to_le_bytes()).collect();
|
||||
let data = e.bytes(which);
|
||||
let (w, h) = (grid.img.width, grid.img.height);
|
||||
let mut acc = e.zeros(w, h);
|
||||
for (fi, f) in frames.iter().enumerate() {
|
||||
let frame = e.g.upload_image(f)?;
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [fi as u32, w, dx as u32, dy as u32];
|
||||
let out = e.scratch(w, h);
|
||||
e.pixels("ftm_displace", &p, &acc, Some(&frame), &out, Some(&data));
|
||||
acc = out;
|
||||
}
|
||||
Some(on_grid(acc, &grid))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Timewarp
|
||||
|
||||
fn timewarp(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
if ctx.env.host.is_none() {
|
||||
return Some(b);
|
||||
}
|
||||
// A Matte Layer splits foreground and background: the CPU's path.
|
||||
if ctx.params.get("matteLayer").and_then(Value::as_layer).is_some() {
|
||||
return None;
|
||||
}
|
||||
let Some(plan) = effectcraft_effects::timewarp_plan(ctx) else { return Some(b) };
|
||||
let mut up: Vec<Option<GpuImage>> = vec![None; plan.frames.len()];
|
||||
let mut frame = |e: &mut Enc, i: usize| -> Option<GpuImage> {
|
||||
if up[i].is_none() {
|
||||
up[i] = Some(e.g.upload_image(&plan.frames[i])?);
|
||||
}
|
||||
up[i].clone()
|
||||
};
|
||||
let mut imgs = vec![];
|
||||
for (step, _) in &plan.steps {
|
||||
let img = match *step {
|
||||
TwStep::Whole(i) => frame(e, i)?,
|
||||
TwStep::Mix(i, j, w) => {
|
||||
let (a, bb) = (frame(e, i)?, frame(e, j)?);
|
||||
let mut p = Params::default();
|
||||
p.f[0][0] = w;
|
||||
kernel(e, "ftm_mix", &p, &a, Some(&bb), None)
|
||||
}
|
||||
TwStep::Motion { a, b: bi, w, flow } => {
|
||||
let pa = e.g.upload_image(&effectcraft_effects::timewarp_crop(&plan.frames[a], &plan.pm))?;
|
||||
let pb = e.g.upload_image(&effectcraft_effects::timewarp_crop(&plan.frames[bi], &plan.pm))?;
|
||||
let f = &plan.flows[flow];
|
||||
let v: Vec<f32> = f.v.iter().flat_map(|q| [q[0], q[1]]).collect();
|
||||
let data = e.data(&v);
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [if f.v.is_empty() { 0 } else { f.cols as u32 }, f.rows as u32, plan.pm.extreme as u32, plan.pm.build_from_one as u32];
|
||||
p.f[0] = [f.block as f32, w, plan.pm.error_threshold, 0.0];
|
||||
kernel(e, "ftm_motion", &p, &pa, Some(&pb), Some(&data))
|
||||
}
|
||||
};
|
||||
imgs.push(img);
|
||||
}
|
||||
let img = if imgs.len() == 1 {
|
||||
imgs.pop()?
|
||||
} else {
|
||||
let weights: Vec<f32> = plan.steps.iter().map(|(_, w)| *w).collect();
|
||||
average(e, &imgs, &weights)?
|
||||
};
|
||||
Some(on_grid(img, &plan.grid))
|
||||
}
|
||||
@@ -0,0 +1,595 @@
|
||||
//! GPU effects, transitions and perspective (kernels in `shaders/fx_transition.wgsl`, every
|
||||
//! entry point prefixed `ftr_`): Block Dissolve, CC Glass Wipe, CC Grid Wipe, CC Image Wipe,
|
||||
//! CC Jaws, CC Line Sweep, CC Radial ScaleWipe, CC Scale Wipe, CC Twister, CC WarpoMatic,
|
||||
//! Radial Shadow, CC Bender, CC Blobbylize, CC Cylinder, CC Sphere, CC Spotlight, CC
|
||||
//! Environment and 3D Glasses, with the CPU effects' exact steps.
|
||||
//!
|
||||
//! Scalars (corner distances, projected extents, light vectors, rotation matrices) are computed
|
||||
//! on the CPU in f64 as the CPU effect does; Block Dissolve's block indices too (one per column
|
||||
//! and row sub-sample), so the hash sees the CPU's exact integers. Other layers (gradient,
|
||||
//! reveal, backside, environment, stereo views) are fetched and fitted on the CPU
|
||||
//! (`util::fit_layer`) and uploaded. Gradient planes are blurred with the CPU's
|
||||
//! `util::gauss_plane` steps (box passes, edges repeated). Card Wipe (a 3D card renderer
|
||||
//! shared with Card Dance and Shatter) stays on the CPU.
|
||||
|
||||
use effectcraft_effects::{Buf, EffectCtx};
|
||||
use effectcraft_raster::Image;
|
||||
|
||||
use crate::context::{Enc, GpuImage, Params};
|
||||
use crate::effects::{GBuf, gaussian_blur};
|
||||
|
||||
/// Compute entry points in `fx_transition.wgsl`.
|
||||
pub(crate) const KERNELS: &[&str] = &[
|
||||
"ftr_plane",
|
||||
"ftr_mul",
|
||||
"ftr_under",
|
||||
"ftr_blocks",
|
||||
"ftr_grid",
|
||||
"ftr_radial_scale",
|
||||
"ftr_scale",
|
||||
"ftr_glass",
|
||||
"ftr_image_wipe",
|
||||
"ftr_jaws",
|
||||
"ftr_line_sweep",
|
||||
"ftr_twister",
|
||||
"ftr_react_contrast",
|
||||
"ftr_react_local",
|
||||
"ftr_warpo",
|
||||
"ftr_rshadow",
|
||||
"ftr_bender",
|
||||
"ftr_blob",
|
||||
"ftr_cylinder",
|
||||
"ftr_sphere",
|
||||
"ftr_spot",
|
||||
"ftr_env",
|
||||
"ftr_shift",
|
||||
"ftr_glasses",
|
||||
];
|
||||
|
||||
/// Effect ids implemented here.
|
||||
pub(crate) const IDS: &[&str] = &[
|
||||
"ec.transition.blockdissolve",
|
||||
"ec.transition.ccglasswipe",
|
||||
"ec.transition.ccgridwipe",
|
||||
"ec.transition.ccimagewipe",
|
||||
"ec.transition.ccjaws",
|
||||
"ec.transition.cclinesweep",
|
||||
"ec.transition.ccradialscalewipe",
|
||||
"ec.transition.ccscalewipe",
|
||||
"ec.transition.cctwister",
|
||||
"ec.transition.ccwarpomatic",
|
||||
"ec.perspective.radialshadow",
|
||||
"ec.distort.ccbender",
|
||||
"ec.distort.ccblobbylize",
|
||||
"ec.perspective.cccylinder",
|
||||
"ec.perspective.ccsphere",
|
||||
"ec.perspective.ccspotlight",
|
||||
"ec.perspective.ccenvironment",
|
||||
"ec.perspective.3dglasses",
|
||||
];
|
||||
|
||||
/// 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.transition.blockdissolve" => block_dissolve(e, ctx, b),
|
||||
"ec.transition.ccglasswipe" => glass_wipe(e, ctx, b),
|
||||
"ec.transition.ccgridwipe" => grid_wipe(e, ctx, b),
|
||||
"ec.transition.ccimagewipe" => image_wipe(e, ctx, b),
|
||||
"ec.transition.ccjaws" => jaws(e, ctx, b),
|
||||
"ec.transition.cclinesweep" => line_sweep(e, ctx, b),
|
||||
"ec.transition.ccradialscalewipe" => radial_scale_wipe(e, ctx, b),
|
||||
"ec.transition.ccscalewipe" => scale_wipe(e, ctx, b),
|
||||
"ec.transition.cctwister" => twister(e, ctx, b),
|
||||
"ec.transition.ccwarpomatic" => warpomatic(e, ctx, b),
|
||||
"ec.perspective.radialshadow" => radial_shadow(e, ctx, b),
|
||||
"ec.distort.ccbender" => bender(e, ctx, b),
|
||||
"ec.distort.ccblobbylize" => blobbylize(e, ctx, b),
|
||||
"ec.perspective.cccylinder" => cylinder(e, ctx, b),
|
||||
"ec.perspective.ccsphere" => sphere(e, ctx, b),
|
||||
"ec.perspective.ccspotlight" => spotlight(e, ctx, b),
|
||||
"ec.perspective.ccenvironment" => environment(e, ctx, b),
|
||||
"ec.perspective.3dglasses" => glasses(e, ctx, b),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- helpers
|
||||
|
||||
/// A same-size per-pixel kernel over `src` (the buffer's size).
|
||||
fn kernel(e: &mut Enc, entry: &str, p: &Params, src: &GpuImage, aux: Option<&GpuImage>, data: Option<&wgpu::Buffer>) -> GpuImage {
|
||||
let out = e.scratch(src.width, src.height);
|
||||
e.pixels(entry, p, src, aux, &out, data);
|
||||
out
|
||||
}
|
||||
|
||||
fn run(e: &mut Enc, entry: &str, p: &Params, mut b: GBuf, aux: Option<&GpuImage>, data: Option<&wgpu::Buffer>) -> Option<GBuf> {
|
||||
b.img = kernel(e, entry, p, &b.img, aux, data);
|
||||
Some(b)
|
||||
}
|
||||
|
||||
fn f4(v: [f64; 4]) -> [f32; 4] {
|
||||
v.map(|x| x as f32)
|
||||
}
|
||||
|
||||
fn rgb(c: [f32; 4], w: f32) -> [f32; 4] {
|
||||
[c[0], c[1], c[2], w]
|
||||
}
|
||||
|
||||
/// The CPU buffer geometry of `b` (no pixels: `fit_layer` reads only size, offset and scale).
|
||||
fn shape(b: &GBuf) -> Buf {
|
||||
Buf { img: Image { width: b.img.width, height: b.img.height, data: vec![] }, offset: b.offset, scale: b.scale }
|
||||
}
|
||||
|
||||
/// The layer in layer parameter `id` fitted to `b` (`util::fit_layer`) and uploaded; `Some(None)`
|
||||
/// when none is chosen.
|
||||
fn fitted(e: &mut Enc, ctx: &EffectCtx, b: &GBuf, id: &str, masks_and_effects: bool, stretch: bool) -> Option<Option<GpuImage>> {
|
||||
match ctx.layer_param(id, masks_and_effects) {
|
||||
Some(o) => Some(Some(e.g.upload_image(&effectcraft_effects::util::fit_layer(ctx, &shape(b), &o, stretch))?)),
|
||||
None => Some(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// `util::layer_or_self`: the fitted layer, or the buffer itself.
|
||||
fn layer_or_self(e: &mut Enc, ctx: &EffectCtx, b: &GBuf, id: &str) -> Option<GpuImage> {
|
||||
Some(fitted(e, ctx, b, id, true, false)?.unwrap_or_else(|| b.img.clone()))
|
||||
}
|
||||
|
||||
/// A plane (x) of `img`: `util::Src` index `src` times alpha (alpha itself unscaled); 11 is CC
|
||||
/// Environment's relief.
|
||||
fn plane(e: &mut Enc, img: &GpuImage, src: u32) -> GpuImage {
|
||||
let mut p = Params::default();
|
||||
p.u[0][0] = src;
|
||||
kernel(e, "ftr_plane", &p, img, None, None)
|
||||
}
|
||||
|
||||
/// transition2::prop_plane's property → `util::Src` index.
|
||||
fn prop_src(prop: u32) -> u32 {
|
||||
match prop {
|
||||
5 => 6,
|
||||
6 => 7,
|
||||
p => p.min(4),
|
||||
}
|
||||
}
|
||||
|
||||
/// `img` as RGBA rows for the kernels' third image (`P.u[3]` = stride, width, height).
|
||||
fn rows(e: &mut Enc, p: &mut Params, img: &GpuImage) -> wgpu::Buffer {
|
||||
let (buf, stride) = e.image_rows(img);
|
||||
p.u[3] = [stride, img.width, img.height, 0];
|
||||
buf
|
||||
}
|
||||
|
||||
fn completion(ctx: &EffectCtx) -> f64 {
|
||||
(ctx.params.f("completion") / 100.0).clamp(0.0, 1.0)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Block Dissolve (transition.rs)
|
||||
|
||||
fn block_dissolve(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let done = completion(ctx) as f32;
|
||||
if done <= 0.0 {
|
||||
return Some(b);
|
||||
}
|
||||
let pr = ctx.params;
|
||||
let bw = (pr.f("blockWidth") * b.scale).max(1e-3);
|
||||
let bh = (pr.f("blockHeight") * b.scale).max(1e-3);
|
||||
let feather = pr.f("feather") * b.scale;
|
||||
let subs: Vec<f64> = if pr.b("softEdges") { (0..4).map(|k| (k as f64 + 0.5) / 4.0).collect() } else { vec![0.5] };
|
||||
// Block indices per column / row sub-sample, as the CPU computes them (f64, then u32).
|
||||
let index =
|
||||
|n: u32, o: f64, size: f64| (0..n).flat_map(|i| subs.iter().map(move |s| (((i as f64 + s - o) / size).floor() as i64) as u32)).collect::<Vec<_>>();
|
||||
let cols = index(b.img.width, b.offset[0], bw);
|
||||
let table: Vec<u8> = cols.iter().chain(&index(b.img.height, b.offset[1], bh)).flat_map(|v| v.to_le_bytes()).collect();
|
||||
let data = e.bytes(table);
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [subs.len() as u32, ctx.seed ^ 0xb10c, cols.len() as u32, 0];
|
||||
p.f[0][0] = done;
|
||||
let mut mask = kernel(e, "ftr_blocks", &p, &b.img, None, Some(&data));
|
||||
if feather > 0.0 {
|
||||
mask = gaussian_blur(e, &mask, feather * 0.5, feather * 0.5, true);
|
||||
}
|
||||
run(e, "ftr_mul", &Params::default(), b, Some(&mask), None)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Grid Wipe / Radial ScaleWipe / Scale Wipe (transition.rs)
|
||||
|
||||
/// transition::max_corner_dist.
|
||||
fn max_corner_dist(b: &GBuf, c: (f64, f64), metric: impl Fn(f64, f64) -> f64) -> f64 {
|
||||
let (w, h) = (b.img.width as f64, b.img.height as f64);
|
||||
[(0.0, 0.0), (w, 0.0), (0.0, h), (w, h)].iter().map(|(x, y)| metric(x - c.0, y - c.1)).fold(1e-6, f64::max)
|
||||
}
|
||||
|
||||
fn grid_wipe(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let done = completion(ctx);
|
||||
if done <= 0.0 {
|
||||
return Some(b);
|
||||
}
|
||||
let pr = ctx.params;
|
||||
let c = b.to_px(pr.v2("center"));
|
||||
let rot = pr.f("rotation").to_radians();
|
||||
let border = pr.f("border").max(0.0) * b.scale;
|
||||
let tiles = pr.f("tiles").round().clamp(1.0, 500.0);
|
||||
let shape = pr.e("shape");
|
||||
let cs = (ctx.layer_size[0] * b.scale / tiles).max(1.0);
|
||||
let (s, co) = rot.sin_cos();
|
||||
let metric = |dx: f64, dy: f64| match shape {
|
||||
0 => dx.abs(),
|
||||
2 => dx.abs().max(dy.abs()),
|
||||
_ => dx.hypot(dy),
|
||||
};
|
||||
let maxd = max_corner_dist(&b, c, |dx, dy| metric(dx * co + dy * s, -dx * s + dy * co));
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [shape, pr.b("reverse") as u32, 0, 0];
|
||||
p.f[0] = f4([c.0, c.1, s, co]);
|
||||
p.f[1] = f4([cs, maxd, done, border]);
|
||||
run(e, "ftr_grid", &p, b, None, None)
|
||||
}
|
||||
|
||||
fn radial_scale_wipe(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let done = completion(ctx);
|
||||
if done <= 0.0 {
|
||||
return Some(b);
|
||||
}
|
||||
let c = b.to_px(ctx.params.v2("center"));
|
||||
let maxd = max_corner_dist(&b, c, f64::hypot);
|
||||
let mut p = Params::default();
|
||||
p.u[0][0] = ctx.params.b("reverse") as u32;
|
||||
p.f[0] = f4([c.0, c.1, maxd, done]);
|
||||
run(e, "ftr_radial_scale", &p, b, None, None)
|
||||
}
|
||||
|
||||
fn scale_wipe(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let stretch = ctx.params.f("stretch").max(0.0);
|
||||
if stretch <= 0.0 {
|
||||
return Some(b);
|
||||
}
|
||||
let c = b.to_px(ctx.params.v2("center"));
|
||||
let a = ctx.params.f("direction").to_radians();
|
||||
let mut p = Params::default();
|
||||
p.f[0] = f4([c.0, c.1, a.sin(), -a.cos()]);
|
||||
p.f[1][0] = stretch as f32;
|
||||
run(e, "ftr_scale", &p, b, None, None)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Glass Wipe / Image Wipe / WarpoMatic (transition2.rs)
|
||||
|
||||
fn glass_wipe(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let c = completion(ctx) as f32;
|
||||
if c <= 0.0 {
|
||||
return Some(b);
|
||||
}
|
||||
let pr = ctx.params;
|
||||
let gimg = layer_or_self(e, ctx, &b, "gradientLayer")?;
|
||||
let mut g = plane(e, &gimg, 4);
|
||||
let sm = pr.f("softness") * b.scale * 0.2;
|
||||
if sm > 0.05 {
|
||||
g = gaussian_blur(e, &g, sm, sm, true);
|
||||
}
|
||||
let soft = (pr.f("softness") / 100.0) as f32;
|
||||
let rev = fitted(e, ctx, &b, "layerToReveal", true, false)?;
|
||||
let mut p = Params::default();
|
||||
let data = rev.as_ref().map(|r| rows(e, &mut p, r));
|
||||
p.u[0][0] = rev.is_some() as u32;
|
||||
p.f[0] = [c, soft * 0.5 + 0.01, (pr.f("displacementAmount") * b.scale * 4.0) as f32, 0.0];
|
||||
run(e, "ftr_glass", &p, b, Some(&g), data.as_ref())
|
||||
}
|
||||
|
||||
fn image_wipe(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let c = completion(ctx) as f32;
|
||||
if c <= 0.0 {
|
||||
return Some(b);
|
||||
}
|
||||
let pr = ctx.params;
|
||||
let gimg = layer_or_self(e, ctx, &b, "layer")?;
|
||||
let mut g = plane(e, &gimg, prop_src(pr.e("property")));
|
||||
let bl = pr.f("blur") * b.scale * 0.5;
|
||||
if bl > 0.05 {
|
||||
g = gaussian_blur(e, &g, bl, bl, true);
|
||||
}
|
||||
let mut w = (pr.f("borderSoftness") / 100.0) as f32 * 0.5;
|
||||
if pr.b("autoSoftness") {
|
||||
w = w.max(0.02);
|
||||
}
|
||||
let mut p = Params::default();
|
||||
p.u[0][0] = pr.b("inverseGradient") as u32;
|
||||
p.f[0] = [c, w, 0.0, 0.0];
|
||||
run(e, "ftr_image_wipe", &p, b, Some(&g), None)
|
||||
}
|
||||
|
||||
fn warpomatic(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let c = completion(ctx) as f32;
|
||||
if c <= 0.0 {
|
||||
return Some(b);
|
||||
}
|
||||
let pr = ctx.params;
|
||||
let rimg = layer_or_self(e, ctx, &b, "reactorLayer")?;
|
||||
let mut m = plane(e, &rimg, 4);
|
||||
let sm = pr.f("smoothness") * b.scale * 0.5;
|
||||
if sm > 0.05 {
|
||||
m = gaussian_blur(e, &m, sm, sm, true);
|
||||
}
|
||||
m = match pr.e("reactor") {
|
||||
1 => {
|
||||
let mean = gaussian_blur(e, &m, 8.0 * b.scale, 8.0 * b.scale, true);
|
||||
kernel(e, "ftr_react_contrast", &Params::default(), &m, Some(&mean), None)
|
||||
}
|
||||
2 => kernel(e, "ftr_react_local", &Params::default(), &m, Some(&m), None),
|
||||
_ => m,
|
||||
};
|
||||
let rev = fitted(e, ctx, &b, "layerToReveal", true, false)?;
|
||||
let mut p = Params::default();
|
||||
let data = rev.as_ref().map(|r| rows(e, &mut p, r));
|
||||
p.u[0] = [pr.e("warpDirection"), rev.is_some() as u32, 0, 0];
|
||||
p.f[0] = [c, (pr.f("blendSpan") / 100.0) as f32 * 0.5, (pr.f("warpAmount") * b.scale) as f32 * 20.0, 0.0];
|
||||
run(e, "ftr_warpo", &p, b, Some(&m), data.as_ref())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Jaws / Line Sweep / Twister (transition2.rs)
|
||||
|
||||
fn jaws(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let c = completion(ctx);
|
||||
if c <= 0.0 {
|
||||
return Some(b);
|
||||
}
|
||||
let pr = ctx.params;
|
||||
let (cx, cy) = b.to_px(pr.v2("center"));
|
||||
let (sn, cs) = pr.f("direction").to_radians().sin_cos();
|
||||
let width = (pr.f("width") * b.scale).max(1.0);
|
||||
let amp = pr.f("height") / 100.0 * width * 0.5;
|
||||
let (bw, bh) = (b.img.width as f64, b.img.height as f64);
|
||||
let vmax = [(0.0, 0.0), (bw, 0.0), (0.0, bh), (bw, bh)].iter().map(|&(x, y)| (-(x - cx) * sn + (y - cy) * cs).abs()).fold(0.0, f64::max);
|
||||
let d = c * (vmax + 2.0 * amp + 2.0);
|
||||
let mut p = Params::default();
|
||||
p.u[0][0] = pr.e("shape");
|
||||
p.f[0] = f4([cx, cy, sn, cs]);
|
||||
p.f[1] = f4([width, amp, d, 0.0]);
|
||||
run(e, "ftr_jaws", &p, b, None, None)
|
||||
}
|
||||
|
||||
fn line_sweep(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let c = completion(ctx);
|
||||
if c <= 0.0 {
|
||||
return Some(b);
|
||||
}
|
||||
let pr = ctx.params;
|
||||
let (sn, cs) = pr.f("direction").to_radians().sin_cos();
|
||||
let thick = (pr.f("thickness") * b.scale).max(1.0);
|
||||
let slant = pr.f("slant") * b.scale;
|
||||
let (w, h) = (b.img.width as f64, b.img.height as f64);
|
||||
let (mut u0, mut u1, mut v0, mut v1) = (f64::MAX, f64::MIN, f64::MAX, f64::MIN);
|
||||
for (x, y) in [(0.0, 0.0), (w, 0.0), (0.0, h), (w, h)] {
|
||||
let (u, v) = (x * cs + y * sn, -x * sn + y * cs);
|
||||
u0 = u0.min(u);
|
||||
u1 = u1.max(u);
|
||||
v0 = v0.min(v);
|
||||
v1 = v1.max(v);
|
||||
}
|
||||
let nstr = ((v1 - v0) / thick).ceil().max(1.0);
|
||||
let total = (u1 - u0) + slant.abs() * nstr + 1.0;
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [pr.b("flipDirection") as u32, (slant >= 0.0) as u32, 0, 0];
|
||||
p.f[0] = f4([sn, cs, v0, u0]);
|
||||
p.f[1] = f4([thick, slant.abs(), nstr, total]);
|
||||
p.f[2][0] = c as f32;
|
||||
run(e, "ftr_line_sweep", &p, b, None, None)
|
||||
}
|
||||
|
||||
fn twister(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let c = completion(ctx);
|
||||
if c <= 0.0 {
|
||||
return Some(b);
|
||||
}
|
||||
let pr = ctx.params;
|
||||
let (cx, cy) = b.to_px(pr.v2("center"));
|
||||
let (sn, cs) = pr.f("axis").to_radians().sin_cos();
|
||||
let half = (b.img.width as f64).hypot(b.img.height as f64) * 0.5;
|
||||
let back = fitted(e, ctx, &b, "backside", true, false)?;
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [back.is_some() as u32, pr.b("shading") as u32, 0, 0];
|
||||
p.f[0] = f4([cx, cy, sn, cs]);
|
||||
p.f[1] = f4([half, c, 0.0, 0.0]);
|
||||
run(e, "ftr_twister", &p, b, back.as_ref(), None)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Radial Shadow / CC Cylinder / CC Sphere / CC Spotlight (perspective.rs)
|
||||
|
||||
fn radial_shadow(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
|
||||
let pr = ctx.params;
|
||||
let color = pr.color("color");
|
||||
let soft = pr.f("softness").max(0.0) * b.scale;
|
||||
let k = 1.0 + pr.f("projectionDistance").max(0.0) / 100.0;
|
||||
let lp0 = pr.v2("lightSource");
|
||||
if pr.b("resizeLayer") && !ctx.adjustment {
|
||||
let l = b.to_px(lp0);
|
||||
let (bw, bh) = (b.img.width as f64, b.img.height as f64);
|
||||
let grow = [(0.0, 0.0), (bw, 0.0), (0.0, bh), (bw, bh)].iter().map(|(cx, cy)| ((cx - l.0).hypot(cy - l.1)) * (k - 1.0)).fold(0.0, f64::max);
|
||||
b.pad(e, (grow + soft * 1.5).ceil() as u32 + 1)?;
|
||||
}
|
||||
let l = b.to_px(lp0);
|
||||
let mut p = Params::default();
|
||||
p.u[0][0] = (pr.e("render") == 1) as u32;
|
||||
p.f[0] = [l.0 as f32, l.1 as f32, k as f32, pr.f("opacity") as f32 / 100.0 * color[3]];
|
||||
p.f[1] = rgb(color, pr.f("colorInfluence") as f32 / 100.0);
|
||||
let mut sh = kernel(e, "ftr_rshadow", &p, &b.img, None, None);
|
||||
if soft > 0.0 {
|
||||
sh = gaussian_blur(e, &sh, soft * 0.5, soft * 0.5, false);
|
||||
}
|
||||
if !pr.b("shadowOnly") {
|
||||
sh = kernel(e, "ftr_under", &Params::default(), &sh, Some(&b.img), None);
|
||||
}
|
||||
b.img = sh;
|
||||
Some(b)
|
||||
}
|
||||
|
||||
fn norm3(v: [f64; 3]) -> [f64; 3] {
|
||||
let l = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt().max(1e-12);
|
||||
[v[0] / l, v[1] / l, v[2] / l]
|
||||
}
|
||||
|
||||
/// perspective::Shading into f[3..7]; the layer rectangle into f[1].
|
||||
fn shading(ctx: &EffectCtx, b: &GBuf, p: &mut Params) {
|
||||
let pr = ctx.params;
|
||||
let (a, el) = (pr.f("lightDirection").to_radians(), pr.f("lightHeight").to_radians());
|
||||
let l = norm3([a.sin() * el.cos(), -a.cos() * el.cos(), el.sin()]);
|
||||
let hv = norm3([l[0], l[1], l[2] + 1.0]);
|
||||
p.f[1] = f4([b.offset[0], b.offset[1], ctx.layer_size[0] * b.scale, ctx.layer_size[1] * b.scale]);
|
||||
p.f[3] = [l[0] as f32, l[1] as f32, l[2] as f32, pr.f("lightIntensity") as f32 / 100.0];
|
||||
p.f[4] = rgb(pr.color("lightColor"), pr.f("ambient") as f32 / 100.0);
|
||||
p.f[5] = [pr.f("diffuse") as f32 / 100.0, pr.f("specular") as f32 / 100.0, 1.0 / (pr.f("roughness") as f32).max(0.001), 0.0];
|
||||
p.f[6] = [hv[0] as f32, hv[1] as f32, hv[2] as f32, 0.0];
|
||||
}
|
||||
|
||||
fn cylinder(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let pr = ctx.params;
|
||||
let (w, h) = (ctx.layer_size[0] * b.scale, ctx.layer_size[1] * b.scale);
|
||||
let r = (pr.f("radius") / 100.0 * w / std::f64::consts::TAU).max(0.5);
|
||||
let c = b.to_px(pr.v2("position"));
|
||||
let mut p = Params::default();
|
||||
shading(ctx, &b, &mut p);
|
||||
p.u[0][0] = pr.e("render");
|
||||
p.f[0] = f4([c.0, c.1, r, h]);
|
||||
p.f[2][0] = pr.f("rotation").to_radians() as f32;
|
||||
run(e, "ftr_cylinder", &p, b, None, None)
|
||||
}
|
||||
|
||||
/// perspective::rot_xyz (R = Rz · Ry · Rx).
|
||||
fn rot_xyz(rx: f64, ry: f64, rz: f64) -> [[f64; 3]; 3] {
|
||||
let (sx, cx) = rx.to_radians().sin_cos();
|
||||
let (sy, cy) = ry.to_radians().sin_cos();
|
||||
let (sz, cz) = rz.to_radians().sin_cos();
|
||||
let mx = [[1.0, 0.0, 0.0], [0.0, cx, -sx], [0.0, sx, cx]];
|
||||
let my = [[cy, 0.0, sy], [0.0, 1.0, 0.0], [-sy, 0.0, cy]];
|
||||
let mz = [[cz, -sz, 0.0], [sz, cz, 0.0], [0.0, 0.0, 1.0]];
|
||||
let mul = |a: [[f64; 3]; 3], b: [[f64; 3]; 3]| {
|
||||
let mut o = [[0.0; 3]; 3];
|
||||
for i in 0..3 {
|
||||
for j in 0..3 {
|
||||
o[i][j] = (0..3).map(|k| a[i][k] * b[k][j]).sum();
|
||||
}
|
||||
}
|
||||
o
|
||||
};
|
||||
mul(mz, mul(my, mx))
|
||||
}
|
||||
|
||||
fn sphere(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let pr = ctx.params;
|
||||
let r = (pr.f("radius") * b.scale).max(0.5);
|
||||
let c = b.to_px(pr.v2("offset"));
|
||||
let m = rot_xyz(pr.f("rotationX"), pr.f("rotationY"), pr.f("rotationZ"));
|
||||
let mut p = Params::default();
|
||||
shading(ctx, &b, &mut p);
|
||||
p.u[0][0] = pr.e("render");
|
||||
p.f[0] = f4([c.0, c.1, r, 0.0]);
|
||||
// Rows of M: the kernel computes Mᵀ · q as q.x·row0 + q.y·row1 + q.z·row2.
|
||||
for (i, row) in m.iter().enumerate() {
|
||||
p.f[7 + i] = f4([row[0], row[1], row[2], 0.0]);
|
||||
}
|
||||
run(e, "ftr_sphere", &p, b, None, None)
|
||||
}
|
||||
|
||||
fn spotlight(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let pr = ctx.params;
|
||||
let (w, h) = (ctx.layer_size[0] * b.scale, ctx.layer_size[1] * b.scale);
|
||||
let from = b.to_px(pr.v2("from"));
|
||||
let to = b.to_px(pr.v2("to"));
|
||||
let height = (pr.f("height") / 100.0 * w.max(h)).max(1e-3);
|
||||
let cone = pr.f("coneAngle").clamp(0.1, 89.9).to_radians();
|
||||
let soft = (pr.f("edgeSoftness") / 100.0).clamp(0.0, 1.0);
|
||||
let axis = norm3([to.0 - from.0, to.1 - from.1, -height]);
|
||||
let mut p = Params::default();
|
||||
p.u[0][0] = (pr.e("render") == 1) as u32;
|
||||
p.f[0] = [from.0 as f32, from.1 as f32, height as f32, pr.f("intensity") as f32 / 100.0];
|
||||
p.f[1] = f4([axis[0], axis[1], axis[2], cone * (1.0 - soft)]);
|
||||
p.f[2] = rgb(pr.color("color"), cone as f32);
|
||||
run(e, "ftr_spot", &p, b, None, None)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Environment / 3D Glasses (perspective2.rs)
|
||||
|
||||
fn environment(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let pr = ctx.params;
|
||||
let (mut env, es, eo, esz) = match ctx.layer_param("environment", true) {
|
||||
Some(o) => (e.g.upload_image(&o.buf.img)?, o.buf.scale, o.buf.offset, o.size),
|
||||
None => (b.img.clone(), b.scale, b.offset, ctx.layer_size),
|
||||
};
|
||||
if pr.b("filterEnvironment") {
|
||||
env = gaussian_blur(e, &env, 2.0 * es, 2.0 * es, true);
|
||||
}
|
||||
let relief = plane(e, &b.img, 11);
|
||||
let soft = 1.5 * b.scale.max(0.25);
|
||||
let hmap = gaussian_blur(e, &relief, soft, soft, true);
|
||||
let mut p = Params::default();
|
||||
let data = rows(e, &mut p, &env);
|
||||
p.u[0][0] = pr.e("mapping");
|
||||
p.f[0] = f4([1.0 / b.scale.max(1e-6), pr.f("height") / 100.0 * 20.0, es, 0.0]);
|
||||
p.f[1] = f4([eo[0], eo[1], esz[0], esz[1]]);
|
||||
run(e, "ftr_env", &p, b, Some(&hmap), Some(&data))
|
||||
}
|
||||
|
||||
fn glasses(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let pr = ctx.params;
|
||||
let mut l = fitted(e, ctx, &b, "leftView", true, false)?.unwrap_or_else(|| b.img.clone());
|
||||
let mut r = fitted(e, ctx, &b, "rightView", true, false)?.unwrap_or_else(|| b.img.clone());
|
||||
if pr.b("leftRightSwap") {
|
||||
std::mem::swap(&mut l, &mut r);
|
||||
}
|
||||
let unit = if pr.e("units") == 1 { ctx.layer_size[0] / 100.0 } else { 1.0 } * b.scale;
|
||||
let conv = pr.f("sceneConvergence") * unit;
|
||||
let va = pr.f("verticalAlignment") * unit;
|
||||
let mut shift = |img: GpuImage, dx: f64, dy: f64| {
|
||||
if dx == 0.0 && dy == 0.0 {
|
||||
return img;
|
||||
}
|
||||
let mut p = Params::default();
|
||||
p.f[0] = f4([dx, dy, 0.0, 0.0]);
|
||||
kernel(e, "ftr_shift", &p, &img, None, None)
|
||||
};
|
||||
let l = shift(l, conv * 0.5, 0.0);
|
||||
let r = shift(r, -conv * 0.5, va);
|
||||
let mut p = Params::default();
|
||||
p.u[0][0] = pr.e("view3d");
|
||||
p.f[0] = [b.img.width as f32, b.img.height as f32, (pr.f("balance") / 20.0).clamp(0.0, 1.0) as f32, 0.0];
|
||||
let out = kernel(e, "ftr_glasses", &p, &l, Some(&r), None);
|
||||
Some(GBuf { img: out, ..b })
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Bender / CC Blobbylize (distort3.rs)
|
||||
|
||||
fn bender(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let pr = ctx.params;
|
||||
let amount = pr.f("amount") / 100.0;
|
||||
if amount == 0.0 {
|
||||
return Some(b);
|
||||
}
|
||||
let top = b.to_px(pr.v2("top"));
|
||||
let base = b.to_px(pr.v2("base"));
|
||||
let (ax, ay) = (top.0 - base.0, top.1 - base.1);
|
||||
let len = (ax * ax + ay * ay).sqrt().max(1e-6);
|
||||
let (ux, uy) = (ax / len, ay / len);
|
||||
let reference = if pr.b("adjustToDistance") { len } else { ctx.layer_size[1] * b.scale };
|
||||
let mut p = Params::default();
|
||||
p.u[0][0] = pr.e("style");
|
||||
p.f[0] = f4([base.0, base.1, ux, uy]);
|
||||
p.f[1] = f4([len, -uy, ux, amount * reference]);
|
||||
run(e, "ftr_bender", &p, b, None, None)
|
||||
}
|
||||
|
||||
fn blobbylize(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let pr = ctx.params;
|
||||
let soft = pr.f("softness").max(0.0) * b.scale;
|
||||
let blob = layer_or_self(e, ctx, &b, "blobLayer")?;
|
||||
let raw = plane(e, &blob, pr.e("property").min(10));
|
||||
let h = gaussian_blur(e, &raw, soft, soft, true);
|
||||
let lpos = b.to_px(pr.v2("lightPosition"));
|
||||
let lheight = pr.f("lightHeight");
|
||||
let (d, el) = (pr.f("lightDirection").to_radians(), (lheight / 100.0).clamp(0.0, 1.0) * std::f64::consts::FRAC_PI_2);
|
||||
let ldir = [d.sin() * el.cos(), -d.cos() * el.cos(), el.sin()];
|
||||
let span = (b.img.width.max(b.img.height) as f64).max(1.0);
|
||||
let mut p = Params::default();
|
||||
p.f[0] = f4([(soft * 0.5).max(1.0), (pr.f("cutAway") / 100.0).clamp(0.0, 0.99), pr.f("lightIntensity") / 100.0, pr.f("ambient") / 100.0]);
|
||||
p.f[1] = f4([pr.f("diffuse") / 100.0, pr.f("specular") / 100.0, (1.0 / pr.f("roughness").clamp(0.001, 1.0)).min(500.0), pr.f("metal") / 100.0]);
|
||||
p.f[2] = f4([lpos.0, lpos.1, lheight / 100.0 * span, (pr.e("lightType") == 1) as u32 as f64]);
|
||||
p.f[3] = f4([ldir[0], ldir[1], ldir[2], 0.0]);
|
||||
p.f[4] = rgb(pr.color("lightColor"), 0.0);
|
||||
run(e, "ftr_blob", &p, b, Some(&h), None)
|
||||
}
|
||||
+20
-5
@@ -13,15 +13,18 @@
|
||||
//! Advanced 3D comps rasterise on a render pipeline (`advanced3d.wgsl`: depth buffer, PBR,
|
||||
//! image-based light, shadow maps) and finish in compute kernels (`adv3d.wgsl`: supersampling
|
||||
//! resolve, motion-blur sub-samples, iris depth of field, encoding, compositing), see
|
||||
//! [`Accelerator::render_3d`] and `Renderer::prepare_adv_run`. Advanced 3D layers with blend
|
||||
//! modes, track mattes or Preserve Transparency still composite on the CPU (their scenes
|
||||
//! render here through [`Accelerator::render_3d`]).
|
||||
//! [`Accelerator::render_3d`] and `Renderer::prepare_adv_run`. Runs with layers on the 2D
|
||||
//! compositing path (blend modes, track mattes, Preserve Transparency:
|
||||
//! `Renderer::split_adv_run`) render each such layer alone, hide it behind the nearer main
|
||||
//! scene and composite it through the 2D path here too; environment backgrounds (Classic and
|
||||
//! Advanced 3D) draw their sky in a kernel (`Renderer::sky_draw`).
|
||||
//!
|
||||
//! 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_distort`, `fx_extra`, `fx_generate`, `fx_key`, `fx_noise`, `fx_stylize`,
|
||||
//! `fx_tone`, `fx_warp`); settings a kernel cannot match fall back to the CPU
|
||||
//! (`fx_color`, `fx_distort`, `fx_extra`, `fx_generate`, `fx_key`, `fx_light`, `fx_noise`,
|
||||
//! `fx_stylize`, `fx_text`, `fx_time`, `fx_tone`, `fx_transition`, `fx_warp`); settings a
|
||||
//! kernel cannot match fall back to the CPU
|
||||
//! (`catalog::gpu_supported`, or `None` from the family's `apply`).
|
||||
//!
|
||||
//! Working textures come from a pool (`context::Pool`): a frame allocates several full-frame
|
||||
@@ -53,9 +56,13 @@ mod fx_distort;
|
||||
mod fx_extra;
|
||||
mod fx_generate;
|
||||
mod fx_key;
|
||||
mod fx_light;
|
||||
mod fx_noise;
|
||||
mod fx_stylize;
|
||||
mod fx_text;
|
||||
mod fx_time;
|
||||
mod fx_tone;
|
||||
mod fx_transition;
|
||||
mod fx_warp;
|
||||
mod ops;
|
||||
mod particles;
|
||||
@@ -309,12 +316,20 @@ mod tests_fx_generate;
|
||||
#[cfg(test)]
|
||||
mod tests_fx_key;
|
||||
#[cfg(test)]
|
||||
mod tests_fx_light;
|
||||
#[cfg(test)]
|
||||
mod tests_fx_noise;
|
||||
#[cfg(test)]
|
||||
mod tests_fx_stylize;
|
||||
#[cfg(test)]
|
||||
mod tests_fx_text;
|
||||
#[cfg(test)]
|
||||
mod tests_fx_time;
|
||||
#[cfg(test)]
|
||||
mod tests_fx_tone;
|
||||
#[cfg(test)]
|
||||
mod tests_fx_transition;
|
||||
#[cfg(test)]
|
||||
mod tests_fx_warp;
|
||||
#[cfg(test)]
|
||||
mod tests_particles;
|
||||
@@ -255,3 +255,24 @@ fn wire(@builtin(global_invocation_id) id: vec3<u32>) {
|
||||
let p = table[id.x];
|
||||
textureStore(out, vec2<i32>(i32(p.x), i32(p.y)), vec4<f32>(1.0));
|
||||
}
|
||||
|
||||
// A layer rendered on its own (color_in, finished) hidden where the run's main scene is nearer
|
||||
// (`adv::draw_run`): depth = its depth, src = the main scene's colour, radius = the main
|
||||
// scene's depth. Pixels it didn't draw (BIG) are never hidden, as ∞ compares on the CPU.
|
||||
// P.u0 = (width, height)
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn occlude(@builtin(global_invocation_id) id: vec3<u32>) {
|
||||
let w = P.u0.x;
|
||||
if (id.x >= w || id.y >= P.u0.y) {
|
||||
return;
|
||||
}
|
||||
let i = id.y * w + id.x;
|
||||
let q = vec2<i32>(i32(id.x), i32(id.y));
|
||||
var p = textureLoad(color_in, q, 0);
|
||||
let z = depth[i];
|
||||
let mz = radius[i];
|
||||
if (z < BIG * 0.5 && mz < z - 0.01 * max(abs(z), 1.0)) {
|
||||
p *= 1.0 - clamp(src[i].w, 0.0, 1.0);
|
||||
}
|
||||
textureStore(out, q, p);
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
// GPU effects, the CC light family: see src/fx_light.rs. CC Light Rays, CC Light Burst 2.5,
|
||||
// CC Light Sweep and CC Light Wipe, each mirroring the CPU effect operation for operation.
|
||||
// Coordinates are pixel centres relative to the effect's centre, so f32 keeps its precision.
|
||||
|
||||
fn flt_inside(p: vec2<i32>) -> bool {
|
||||
let d = out_dims();
|
||||
return p.x < d.x && p.y < d.y;
|
||||
}
|
||||
|
||||
fn flt_rel(p: vec2<i32>, c: vec2<f32>) -> vec2<f32> {
|
||||
return vec2<f32>(f32(p.x) + 0.5, f32(p.y) + 0.5) - c;
|
||||
}
|
||||
|
||||
// util::smoothstep.
|
||||
fn flt_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);
|
||||
}
|
||||
|
||||
// util::unpremul's colour.
|
||||
fn flt_unpremul(p: vec4<f32>) -> vec3<f32> {
|
||||
if (p.w > 1e-6) {
|
||||
return p.xyz / p.w;
|
||||
}
|
||||
return vec3<f32>(0.0);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Light Rays (generate2::light_rays)
|
||||
// u[0] = (square, colour from source, transfer mode); f[0] = (cx, cy, k, gain);
|
||||
// f[1] = (radius, outer radius + 1e-3); f[2] = colour.
|
||||
|
||||
fn flt_ray_mask(q: vec2<f32>) -> f32 {
|
||||
let d = abs(q);
|
||||
var r = length(d);
|
||||
if (P.u[0].x != 0u) {
|
||||
r = max(d.x, d.y);
|
||||
}
|
||||
return 1.0 - flt_smoothstep(P.f[1].x, P.f[1].y, r);
|
||||
}
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn flt_rays(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!flt_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let c = P.f[0].xy;
|
||||
let rel = flt_rel(p, c);
|
||||
var acc = vec4<f32>(0.0);
|
||||
for (var i = 0u; i < 32u; i++) {
|
||||
let s = 1.0 - P.f[0].z * f32(i) / 32.0;
|
||||
let q = rel * s;
|
||||
let m = flt_ray_mask(q);
|
||||
if (m <= 0.0) {
|
||||
continue;
|
||||
}
|
||||
acc += sample_bilinear(src, c.x + q.x, c.y + q.y) * m;
|
||||
}
|
||||
var ray = acc / 32.0 * P.f[0].w;
|
||||
if (P.u[0].y == 0u) {
|
||||
let l = luminance(flt_unpremul(ray)) * ray.w;
|
||||
ray = vec4<f32>(P.f[2].xyz * l, ray.w);
|
||||
}
|
||||
let la = min(ray.w, 1.0);
|
||||
let px = textureLoad(src, p, 0);
|
||||
var o: vec4<f32>;
|
||||
switch P.u[0].z {
|
||||
case 1u: {
|
||||
o = vec4<f32>(max(px.xyz, ray.xyz), max(px.w, la));
|
||||
}
|
||||
case 2u: {
|
||||
let r4 = vec4<f32>(ray.xyz, la);
|
||||
o = px + r4 - px * r4;
|
||||
}
|
||||
case 3u: {
|
||||
o = vec4<f32>(ray.xyz, la);
|
||||
}
|
||||
default: {
|
||||
let a = clamp(la, 0.0, 1.0);
|
||||
o = vec4<f32>(px.xyz + ray.xyz, clamp(px.w + a * (1.0 - px.w), 0.0, 1.0));
|
||||
}
|
||||
}
|
||||
textureStore(out, p, o);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Light Burst 2.5 (generate2::light_burst)
|
||||
// u[0] = (burst, set colour); f[0] = (cx, cy, ray length k, intensity); f[1] = colour.
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn flt_burst(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!flt_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let c = P.f[0].xy;
|
||||
let rel = flt_rel(p, c);
|
||||
let mode = P.u[0].x;
|
||||
var acc = vec4<f32>(0.0);
|
||||
var wsum = 0.0;
|
||||
var best = vec4<f32>(0.0);
|
||||
var best_l = -1.0;
|
||||
for (var i = 0u; i < 32u; i++) {
|
||||
let f = f32(i) / 32.0;
|
||||
let s = 1.0 - P.f[0].z * f;
|
||||
let q = rel * s;
|
||||
let v = sample_bilinear(src, c.x + q.x, c.y + q.y);
|
||||
if (mode == 0u) {
|
||||
let l = luminance(v.xyz) + v.w * 1e-3;
|
||||
if (l > best_l) {
|
||||
best_l = l;
|
||||
best = v;
|
||||
}
|
||||
} else if (mode == 1u) {
|
||||
let w = 1.0 - f;
|
||||
acc += v * w;
|
||||
wsum += w;
|
||||
} else {
|
||||
acc += v;
|
||||
wsum += 1.0;
|
||||
}
|
||||
}
|
||||
var o = best;
|
||||
if (mode != 0u) {
|
||||
o = acc / max(wsum, 1e-6);
|
||||
}
|
||||
if (P.u[0].y != 0u) {
|
||||
let a = max(o.w, 0.0);
|
||||
let l = luminance(flt_unpremul(o)) * a;
|
||||
o = vec4<f32>(P.f[1].xyz * l, a);
|
||||
}
|
||||
let k = P.f[0].w;
|
||||
textureStore(out, p, vec4<f32>(o.xyz * k, clamp(o.w * k, 0.0, 1.0)));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Light Sweep (generate2::light_sweep)
|
||||
// aux = the (softened) alpha plane in x. u[0] = (shape, reception mode); f[0] = (cx, cy, nx, ny);
|
||||
// f[1] = (half width, sweep, edge intensity); f[2] = light colour.
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn flt_sweep(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!flt_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let px = textureLoad(src, p, 0);
|
||||
let rel = flt_rel(p, P.f[0].xy);
|
||||
let d = abs(rel.x * P.f[0].z + rel.y * P.f[0].w);
|
||||
let half = P.f[1].x;
|
||||
let mode = P.u[0].y;
|
||||
var band: f32;
|
||||
switch P.u[0].x {
|
||||
case 0u: {
|
||||
band = max(1.0 - d / half, 0.0);
|
||||
}
|
||||
case 2u: {
|
||||
band = clamp(half - d + 0.5, 0.0, 1.0);
|
||||
}
|
||||
default: {
|
||||
band = flt_smoothstep(half, 0.0, d);
|
||||
}
|
||||
}
|
||||
if (band <= 0.0) {
|
||||
textureStore(out, p, select(px, vec4<f32>(0.0), mode == 2u));
|
||||
return;
|
||||
}
|
||||
let gx = tex_get_clamped(aux, p.x + 1, p.y).x - tex_get_clamped(aux, p.x - 1, p.y).x;
|
||||
let gy = tex_get_clamped(aux, p.x, p.y + 1).x - tex_get_clamped(aux, p.x, p.y - 1).x;
|
||||
let edge = min(sqrt(gx * gx + gy * gy) * 2.0, 1.0);
|
||||
let light = band * P.f[1].y + band * edge * P.f[1].z;
|
||||
let lc = P.f[2].xyz;
|
||||
let a = px.w;
|
||||
var o: vec4<f32>;
|
||||
if (mode == 1u) {
|
||||
let la = min(band * P.f[1].y, 1.0);
|
||||
o = vec4<f32>(px.xyz + lc * light, min(a + la * (1.0 - a), 1.0));
|
||||
} else if (mode == 2u) {
|
||||
let la = min(light * a, 1.0);
|
||||
o = vec4<f32>(lc * la, la);
|
||||
} else {
|
||||
o = vec4<f32>(px.xyz + lc * light * a, a);
|
||||
}
|
||||
textureStore(out, p, o);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Light Wipe (transition::light_wipe)
|
||||
// u[0] = (shape, colour from source, reverse); f[0] = (cx, cy, sin, cos);
|
||||
// f[1] = (edge, band, intensity × ramp); f[2] = colour.
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn flt_wipe(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!flt_inside(p)) {
|
||||
return;
|
||||
}
|
||||
var px = textureLoad(src, p, 0);
|
||||
let d = flt_rel(p, P.f[0].xy);
|
||||
let s = P.f[0].z;
|
||||
let co = P.f[0].w;
|
||||
let q = vec2<f32>(d.x * co + d.y * s, -d.x * s + d.y * co);
|
||||
var m: f32;
|
||||
switch P.u[0].x {
|
||||
case 0u: {
|
||||
m = abs(q.x);
|
||||
}
|
||||
case 2u: {
|
||||
m = max(abs(q.x), abs(q.y));
|
||||
}
|
||||
default: {
|
||||
m = length(q);
|
||||
}
|
||||
}
|
||||
let edge = P.f[1].x;
|
||||
let visible = select(m > edge, m < edge, P.u[0].z != 0u);
|
||||
if (!visible) {
|
||||
textureStore(out, p, vec4<f32>(0.0));
|
||||
return;
|
||||
}
|
||||
let z = (m - edge) / P.f[1].y;
|
||||
let g = exp(-(z * z)) * P.f[1].z;
|
||||
if (g > 0.0) {
|
||||
var lc = P.f[2].xyz;
|
||||
if (P.u[0].y != 0u) {
|
||||
lc = flt_unpremul(px);
|
||||
}
|
||||
px = vec4<f32>(px.xyz + lc * g * px.w, px.w);
|
||||
}
|
||||
textureStore(out, p, px);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// GPU effects, Numbers and Timecode: see src/fx_text.rs. The glyph coverage is rasterised on
|
||||
// the CPU (aux: fill in x, stroke ring in y, opacity applied); this kernel composites it as
|
||||
// textfx::draw_plan does: the layer or transparency, Timecode's box, then fill and stroke.
|
||||
// u[0] = (display options, keep the layer, box); f[0] = fill colour; f[1] = stroke colour;
|
||||
// f[2] = box (x0, x1, y0, y1); f[3] = (box colour, box opacity).
|
||||
|
||||
fn ftx_over(dst: vec4<f32>, s: vec4<f32>) -> vec4<f32> {
|
||||
return s + dst * (1.0 - s.w);
|
||||
}
|
||||
|
||||
// textfx::tint.
|
||||
fn ftx_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 ftx_text(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
let dims = out_dims();
|
||||
if (p.x >= dims.x || p.y >= dims.y) {
|
||||
return;
|
||||
}
|
||||
var o = vec4<f32>(0.0);
|
||||
if (P.u[0].y != 0u) {
|
||||
o = textureLoad(src, p, 0);
|
||||
}
|
||||
if (P.u[0].z != 0u) {
|
||||
let r = P.f[2];
|
||||
let fy = f32(p.y) + 0.5;
|
||||
let cy = clamp(fy - r.z + 0.5, 0.0, 1.0) * clamp(r.w - fy + 0.5, 0.0, 1.0);
|
||||
if (cy > 0.0) {
|
||||
let fx = f32(p.x) + 0.5;
|
||||
let cx = clamp(fx - r.x + 0.5, 0.0, 1.0) * clamp(r.y - fx + 0.5, 0.0, 1.0);
|
||||
let k = cx * cy * P.f[3].w;
|
||||
if (k > 0.0) {
|
||||
o = ftx_over(o, vec4<f32>(P.f[3].xyz * k, k));
|
||||
}
|
||||
}
|
||||
}
|
||||
let cov = textureLoad(aux, p, 0);
|
||||
let fill = ftx_tint(P.f[0], cov.x);
|
||||
let stroke = ftx_tint(P.f[1], cov.y);
|
||||
switch P.u[0].x {
|
||||
case 1u: {
|
||||
o = ftx_over(o, stroke);
|
||||
}
|
||||
case 2u: {
|
||||
o = ftx_over(ftx_over(o, stroke), fill);
|
||||
}
|
||||
case 3u: {
|
||||
o = ftx_over(ftx_over(o, fill), stroke);
|
||||
}
|
||||
default: {
|
||||
o = ftx_over(o, fill);
|
||||
}
|
||||
}
|
||||
textureStore(out, p, o);
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
// GPU effects, time: see src/fx_time.rs. The frames come from the effect host (fetched and
|
||||
// placed on one grid on the CPU, as Echo's); these kernels combine them as the CPU effects do.
|
||||
|
||||
fn ftm_inside(p: vec2<i32>) -> bool {
|
||||
let d = out_dims();
|
||||
return p.x < d.x && p.y < d.y;
|
||||
}
|
||||
|
||||
// util::unpremul: (straight colour, alpha).
|
||||
fn ftm_unpremul(p: vec4<f32>) -> vec4<f32> {
|
||||
if (p.w > 1e-6) {
|
||||
return vec4<f32>(p.xyz / p.w, p.w);
|
||||
}
|
||||
return vec4<f32>(0.0, 0.0, 0.0, max(p.w, 0.0));
|
||||
}
|
||||
|
||||
// Weighted sum, one frame per pass (time_fx::average): src + aux × f[0].x.
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn ftm_acc(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!ftm_inside(p)) {
|
||||
return;
|
||||
}
|
||||
textureStore(out, p, textureLoad(src, p, 0) + textureLoad(aux, p, 0) * P.f[0].x);
|
||||
}
|
||||
|
||||
// Cross-fade (raster::flow::mix): src + (aux − src) × f[0].x.
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn ftm_mix(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!ftm_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let a = textureLoad(src, p, 0);
|
||||
textureStore(out, p, a + (textureLoad(aux, p, 0) - a) * P.f[0].x);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Time Difference (time_fx::time_difference)
|
||||
// src = the layer now, aux = the target. u[0] = (absolute difference, alpha channel);
|
||||
// f[0].x = contrast gain.
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn ftm_difference(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!ftm_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let cu = ftm_unpremul(textureLoad(src, p, 0));
|
||||
let tu = ftm_unpremul(textureLoad(aux, p, 0));
|
||||
let c = cu.xyz;
|
||||
let d = tu.xyz;
|
||||
let ca = cu.w;
|
||||
let da = tu.w;
|
||||
let gain = P.f[0].x;
|
||||
let diff = c - d;
|
||||
var rgb = clamp(0.5 + diff * 0.5 * gain, vec3<f32>(0.0), vec3<f32>(1.0));
|
||||
if (P.u[0].x != 0u) {
|
||||
rgb = clamp(abs(diff) * gain, vec3<f32>(0.0), vec3<f32>(1.0));
|
||||
}
|
||||
let mx = max(max(rgb.x, rgb.y), rgb.z);
|
||||
let light = (mx + min(min(rgb.x, rgb.y), rgb.z)) * 0.5;
|
||||
var a: f32;
|
||||
switch P.u[0].y {
|
||||
case 1u: {
|
||||
a = da;
|
||||
}
|
||||
case 2u: {
|
||||
a = (ca + da) * 0.5;
|
||||
}
|
||||
case 3u: {
|
||||
a = max(ca, da);
|
||||
}
|
||||
case 4u: {
|
||||
a = 1.0;
|
||||
}
|
||||
case 5u: {
|
||||
a = light;
|
||||
}
|
||||
case 6u: {
|
||||
a = mx;
|
||||
}
|
||||
case 7u: {
|
||||
a = abs(ca - da) * gain;
|
||||
}
|
||||
case 8u: {
|
||||
a = clamp(abs(ca - da) * gain, 0.0, 1.0);
|
||||
rgb = vec3<f32>(1.0);
|
||||
}
|
||||
default: {
|
||||
a = ca;
|
||||
}
|
||||
}
|
||||
a = clamp(a, 0.0, 1.0);
|
||||
textureStore(out, p, vec4<f32>(rgb * a, a));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Time Displacement (time_fx::time_displacement)
|
||||
// One pass per sampled time: pixels whose frame index (data, u32 per pixel, row length u[0].y)
|
||||
// is u[0].x take frame aux at (x + dx, y + dy) (u[0].zw, i32), the others keep src.
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn ftm_displace(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!ftm_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let k = bitcast<u32>(data[u32(p.y) * P.u[0].y + u32(p.x)]);
|
||||
if (k == P.u[0].x) {
|
||||
textureStore(out, p, tex_get(aux, p.x + bitcast<i32>(P.u[0].z), p.y + bitcast<i32>(P.u[0].w)));
|
||||
} else {
|
||||
textureStore(out, p, textureLoad(src, p, 0));
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Timewarp Pixel Motion (time_fx::timewarp_interpolate)
|
||||
// src = frame a, aux = frame b (both source-cropped); data = the motion vectors (x, y pairs).
|
||||
// u[0] = (columns, rows, extreme filtering, build from one image); f[0] = (vector spacing,
|
||||
// fraction w, error threshold).
|
||||
|
||||
// raster::flow::Flow::at.
|
||||
fn ftm_flow_at(x: f32, y: f32) -> vec2<f32> {
|
||||
let cols = P.u[0].x;
|
||||
let rows = P.u[0].y;
|
||||
if (cols == 0u || rows == 0u) {
|
||||
return vec2<f32>(0.0);
|
||||
}
|
||||
let b = P.f[0].x;
|
||||
let fx = clamp(x / b - 0.5, 0.0, f32(cols - 1u));
|
||||
let fy = clamp(y / b - 0.5, 0.0, f32(rows - 1u));
|
||||
let x0 = u32(floor(fx));
|
||||
let y0 = u32(floor(fy));
|
||||
let x1 = min(x0 + 1u, cols - 1u);
|
||||
let y1 = min(y0 + 1u, rows - 1u);
|
||||
let tx = fx - f32(x0);
|
||||
let ty = fy - f32(y0);
|
||||
let g00 = vec2<f32>(data[(y0 * cols + x0) * 2u], data[(y0 * cols + x0) * 2u + 1u]);
|
||||
let g10 = vec2<f32>(data[(y0 * cols + x1) * 2u], data[(y0 * cols + x1) * 2u + 1u]);
|
||||
let g01 = vec2<f32>(data[(y1 * cols + x0) * 2u], data[(y1 * cols + x0) * 2u + 1u]);
|
||||
let g11 = vec2<f32>(data[(y1 * cols + x1) * 2u], data[(y1 * cols + x1) * 2u + 1u]);
|
||||
let top = g00 + (g10 - g00) * tx;
|
||||
let bot = g01 + (g11 - g01) * tx;
|
||||
return top + (bot - top) * ty;
|
||||
}
|
||||
|
||||
fn ftm_sample(t: texture_2d<f32>, x: f32, y: f32) -> vec4<f32> {
|
||||
if (P.u[0].z != 0u) {
|
||||
return sample_bicubic(t, x, y);
|
||||
}
|
||||
return sample_bilinear_clamped(t, x, y);
|
||||
}
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn ftm_motion(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!ftm_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let cx = f32(p.x) + 0.5;
|
||||
let cy = f32(p.y) + 0.5;
|
||||
let f = ftm_flow_at(cx, cy);
|
||||
let w = P.f[0].y;
|
||||
var pa = ftm_sample(src, cx - w * f.x, cy - w * f.y);
|
||||
var pb = ftm_sample(aux, cx + (1.0 - w) * f.x, cy + (1.0 - w) * f.y);
|
||||
if (P.u[0].w != 0u) {
|
||||
textureStore(out, p, select(pb, pa, w < 0.5));
|
||||
return;
|
||||
}
|
||||
let d = abs(pa - pb);
|
||||
let err = max(max(d.x, d.y), max(d.z, d.w));
|
||||
let thr = P.f[0].z;
|
||||
if (thr > 0.0 && err > thr && thr < 1.0) {
|
||||
pa = textureLoad(src, p, 0);
|
||||
pb = textureLoad(aux, p, 0);
|
||||
}
|
||||
textureStore(out, p, pa + (pb - pa) * w);
|
||||
}
|
||||
@@ -0,0 +1,883 @@
|
||||
// GPU effects, transitions and perspective: see src/fx_transition.rs. Block Dissolve, the CC
|
||||
// transitions (Glass Wipe, Grid Wipe, Image Wipe, Jaws, Line Sweep, Radial ScaleWipe, Scale
|
||||
// Wipe, Twister, WarpoMatic), Radial Shadow, CC Bender, CC Blobbylize, CC Cylinder, CC Sphere,
|
||||
// CC Spotlight, CC Environment and 3D Glasses, each mirroring the CPU effect operation for
|
||||
// operation. Coordinates are pixel centres relative to the effect's centre where it has one.
|
||||
|
||||
fn ftr_inside(p: vec2<i32>) -> bool {
|
||||
let d = out_dims();
|
||||
return p.x < d.x && p.y < d.y;
|
||||
}
|
||||
|
||||
fn ftr_centre(p: vec2<i32>) -> vec2<f32> {
|
||||
return vec2<f32>(f32(p.x) + 0.5, f32(p.y) + 0.5);
|
||||
}
|
||||
|
||||
// util::smoothstep.
|
||||
fn ftr_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);
|
||||
}
|
||||
|
||||
// util::unpremul's colour.
|
||||
fn ftr_unpremul(p: vec4<f32>) -> vec3<f32> {
|
||||
if (p.w > 1e-6) {
|
||||
return p.xyz / p.w;
|
||||
}
|
||||
return vec3<f32>(0.0);
|
||||
}
|
||||
|
||||
// The third image, copied into `data` as RGBA f32 rows: P.u[3] = (row stride, width, height).
|
||||
fn ftr_row(x: i32, y: i32) -> vec4<f32> {
|
||||
if (x < 0 || y < 0 || x >= i32(P.u[3].y) || y >= i32(P.u[3].z)) {
|
||||
return vec4<f32>(0.0);
|
||||
}
|
||||
let i = (u32(y) * P.u[3].x + u32(x)) * 4u;
|
||||
return vec4<f32>(data[i], data[i + 1u], data[i + 2u], data[i + 3u]);
|
||||
}
|
||||
|
||||
fn ftr_row_clamped(x: i32, y: i32) -> vec4<f32> {
|
||||
return ftr_row(clamp(x, 0, i32(P.u[3].y) - 1), clamp(y, 0, i32(P.u[3].z) - 1));
|
||||
}
|
||||
|
||||
// Image::sample_bilinear on the third image.
|
||||
fn ftr_row_bilinear(x: f32, y: f32) -> vec4<f32> {
|
||||
let fx = x - 0.5;
|
||||
let fy = y - 0.5;
|
||||
let x0 = floor(fx);
|
||||
let y0 = floor(fy);
|
||||
let tx = fx - x0;
|
||||
let ty = fy - y0;
|
||||
let xi = i32(x0);
|
||||
let yi = i32(y0);
|
||||
let a = ftr_row(xi, yi);
|
||||
let b = ftr_row(xi + 1, yi);
|
||||
let c = ftr_row(xi, yi + 1);
|
||||
let d = ftr_row(xi + 1, yi + 1);
|
||||
let top = a + (b - a) * tx;
|
||||
let bot = c + (d - c) * tx;
|
||||
return top + (bot - top) * ty;
|
||||
}
|
||||
|
||||
// Image::sample_bilinear_clamped on the third image.
|
||||
fn ftr_row_bilinear_clamped(x: f32, y: f32) -> vec4<f32> {
|
||||
let fx = x - 0.5;
|
||||
let fy = y - 0.5;
|
||||
let x0 = floor(fx);
|
||||
let y0 = floor(fy);
|
||||
let tx = fx - x0;
|
||||
let ty = fy - y0;
|
||||
let xi = i32(x0);
|
||||
let yi = i32(y0);
|
||||
let a = ftr_row_clamped(xi, yi);
|
||||
let b = ftr_row_clamped(xi + 1, yi);
|
||||
let c = ftr_row_clamped(xi, yi + 1);
|
||||
let d = ftr_row_clamped(xi + 1, yi + 1);
|
||||
let top = a + (b - a) * tx;
|
||||
let bot = c + (d - c) * tx;
|
||||
return top + (bot - top) * ty;
|
||||
}
|
||||
|
||||
// Central differences of the aux plane (x), edges clamped (transition2::grad).
|
||||
fn ftr_grad(p: vec2<i32>) -> vec2<f32> {
|
||||
let gx = (tex_get_clamped(aux, p.x + 1, p.y).x - tex_get_clamped(aux, p.x - 1, p.y).x) * 0.5;
|
||||
let gy = (tex_get_clamped(aux, p.x, p.y + 1).x - tex_get_clamped(aux, p.x, p.y - 1).x) * 0.5;
|
||||
return vec2<f32>(gx, gy);
|
||||
}
|
||||
|
||||
// transition2::wipe_t.
|
||||
fn ftr_wipe_t(g: f32, c: f32, w0: f32) -> f32 {
|
||||
let w = max(w0, 1e-3);
|
||||
let edge = c * (1.0 + 2.0 * w) - w;
|
||||
return 1.0 - ftr_smoothstep(edge - w, edge + w, g);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- planes
|
||||
|
||||
// A plane (x) from the source: u[0].x = util::Src index (Red, Green, Blue, Alpha, Luminance,
|
||||
// Hue, Lightness, Saturation, Full, Half, Off) times alpha except for Alpha itself
|
||||
// (transition2::prop_plane, CC Blobbylize); 11 = CC Environment's relief.
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn ftr_plane(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!ftr_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let px = textureLoad(src, p, 0);
|
||||
let c = ftr_unpremul(px);
|
||||
let a = max(px.w, 0.0);
|
||||
var v: f32;
|
||||
switch P.u[0].x {
|
||||
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).x;
|
||||
}
|
||||
case 6u: {
|
||||
v = rgb_to_hsl(c).z;
|
||||
}
|
||||
case 7u: {
|
||||
v = rgb_to_hsl(c).y;
|
||||
}
|
||||
case 8u: {
|
||||
v = 1.0;
|
||||
}
|
||||
case 9u: {
|
||||
v = 0.5;
|
||||
}
|
||||
case 11u: {
|
||||
v = luminance(c) * 0.5 + 0.5;
|
||||
}
|
||||
default: {
|
||||
v = 0.0;
|
||||
}
|
||||
}
|
||||
if (P.u[0].x != 3u) {
|
||||
v *= a;
|
||||
}
|
||||
textureStore(out, p, vec4<f32>(v, 0.0, 0.0, 0.0));
|
||||
}
|
||||
|
||||
// src × aux.x (Image::mul_mask).
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn ftr_mul(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!ftr_inside(p)) {
|
||||
return;
|
||||
}
|
||||
textureStore(out, p, textureLoad(src, p, 0) * textureLoad(aux, p, 0).x);
|
||||
}
|
||||
|
||||
// aux + src × (1 − aux.a): src composited under aux.
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn ftr_under(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!ftr_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let o = textureLoad(aux, p, 0);
|
||||
textureStore(out, p, o + textureLoad(src, p, 0) * (1.0 - o.w));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Block Dissolve (transition::block_dissolve)
|
||||
// The kept-block mask (x). u[0] = (sub-samples per axis, seed, column table length);
|
||||
// f[0].x = completion; data = block columns (width × n, u32) then block rows (height × n).
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn ftr_blocks(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!ftr_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let n = P.u[0].x;
|
||||
var hits = 0u;
|
||||
for (var j = 0u; j < n; j++) {
|
||||
let by = bitcast<u32>(data[P.u[0].z + u32(p.y) * n + j]);
|
||||
for (var i = 0u; i < n; i++) {
|
||||
let bx = bitcast<u32>(data[u32(p.x) * n + i]);
|
||||
if (hash_noise(bx, by, P.u[0].y) >= P.f[0].x) {
|
||||
hits += 1u;
|
||||
}
|
||||
}
|
||||
}
|
||||
textureStore(out, p, vec4<f32>(f32(hits) / f32(n * n), 0.0, 0.0, 0.0));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Grid Wipe (transition::grid_wipe)
|
||||
// u[0] = (shape, reverse); f[0] = (cx, cy, sin, cos); f[1] = (cell size, max distance,
|
||||
// completion, border).
|
||||
|
||||
fn ftr_metric(shape: u32, d: vec2<f32>) -> f32 {
|
||||
switch shape {
|
||||
case 0u: {
|
||||
return abs(d.x);
|
||||
}
|
||||
case 2u: {
|
||||
return max(abs(d.x), abs(d.y));
|
||||
}
|
||||
default: {
|
||||
return length(d);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn ftr_grid(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!ftr_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let px = textureLoad(src, p, 0);
|
||||
let d = ftr_centre(p) - P.f[0].xy;
|
||||
let s = P.f[0].z;
|
||||
let co = P.f[0].w;
|
||||
let q = vec2<f32>(d.x * co + d.y * s, -d.x * s + d.y * co);
|
||||
let cs = P.f[1].x;
|
||||
let cc = (floor(q / cs) + 0.5) * cs;
|
||||
var o = clamp(ftr_metric(P.u[0].x, cc) / P.f[1].y, 0.0, 1.0);
|
||||
if (P.u[0].y != 0u) {
|
||||
o = 1.0 - o;
|
||||
}
|
||||
let spread = 0.25;
|
||||
let pr = clamp((P.f[1].z * (1.0 + spread) - o) / spread, 0.0, 1.0);
|
||||
if (pr <= 0.0) {
|
||||
textureStore(out, p, px);
|
||||
return;
|
||||
}
|
||||
let half = cs * 0.5 * (1.0 - pr);
|
||||
let m = max(abs(q.x - cc.x), abs(q.y - cc.y));
|
||||
var k = select(0.0, 1.0, m < half);
|
||||
if (P.f[1].w > 0.0) {
|
||||
k = clamp((half - m) / P.f[1].w + 0.5, 0.0, 1.0);
|
||||
}
|
||||
textureStore(out, p, px * k);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Radial ScaleWipe / CC Scale Wipe (transition.rs)
|
||||
// Radial: u[0].x = reverse; f[0] = (cx, cy, max distance, completion).
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn ftr_radial_scale(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!ftr_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let c = P.f[0].xy;
|
||||
let maxd = P.f[0].z;
|
||||
let done = P.f[0].w;
|
||||
let d = ftr_centre(p) - c;
|
||||
let r = length(d);
|
||||
var o = vec4<f32>(0.0);
|
||||
if (P.u[0].x != 0u) {
|
||||
let lim = (1.0 - done) * maxd;
|
||||
if (!(r >= lim || lim <= 1e-9)) {
|
||||
let k = maxd / lim;
|
||||
o = sample_bilinear(src, c.x + d.x * k, c.y + d.y * k);
|
||||
}
|
||||
} else {
|
||||
let hole = done * maxd;
|
||||
if (!(r <= hole || maxd - hole <= 1e-9)) {
|
||||
let rs = (r - hole) * maxd / (maxd - hole);
|
||||
o = sample_bilinear(src, c.x + d.x / r * rs, c.y + d.y / r * rs);
|
||||
}
|
||||
}
|
||||
textureStore(out, p, o);
|
||||
}
|
||||
|
||||
// Scale Wipe: f[0] = (cx, cy, direction x, direction y); f[1].x = stretch.
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn ftr_scale(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!ftr_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let f = ftr_centre(p);
|
||||
let dir = P.f[0].zw;
|
||||
let u = dot(f - P.f[0].xy, dir);
|
||||
var q = f;
|
||||
if (u > 0.0) {
|
||||
let back = u - u / (1.0 + P.f[1].x);
|
||||
q = f - dir * back;
|
||||
}
|
||||
textureStore(out, p, sample_bilinear_clamped(src, q.x, q.y));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Glass Wipe (transition2::glass_wipe)
|
||||
// aux = the gradient plane (x); data = the layer to reveal (rows). u[0].x = has reveal;
|
||||
// f[0] = (completion, band half-width, displacement).
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn ftr_glass(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!ftr_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let t = ftr_wipe_t(textureLoad(aux, p, 0).x, P.f[0].x, P.f[0].y);
|
||||
let band = 4.0 * t * (1.0 - t);
|
||||
let g = ftr_grad(p);
|
||||
let f = ftr_centre(p);
|
||||
let o = g * P.f[0].z * band * 10.0;
|
||||
var a = textureLoad(src, p, 0);
|
||||
var r = vec4<f32>(0.0);
|
||||
if (band > 1e-4) {
|
||||
a = sample_bilinear(src, f.x + o.x, f.y + o.y);
|
||||
if (P.u[0].x != 0u) {
|
||||
r = ftr_row_bilinear(f.x - o.x, f.y - o.y);
|
||||
}
|
||||
} else if (P.u[0].x != 0u) {
|
||||
r = ftr_row(p.x, p.y);
|
||||
}
|
||||
textureStore(out, p, a + (r - a) * t);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Image Wipe (transition2::image_wipe)
|
||||
// aux = the gradient plane (x). u[0].x = inverse; f[0] = (completion, band half-width).
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn ftr_image_wipe(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!ftr_inside(p)) {
|
||||
return;
|
||||
}
|
||||
var g = textureLoad(aux, p, 0).x;
|
||||
if (P.u[0].x != 0u) {
|
||||
g = 1.0 - g;
|
||||
}
|
||||
let k = 1.0 - ftr_wipe_t(g, P.f[0].x, P.f[0].y);
|
||||
textureStore(out, p, textureLoad(src, p, 0) * k);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Jaws (transition2::jaws)
|
||||
// u[0].x = shape; f[0] = (cx, cy, sin, cos); f[1] = (tooth width, amplitude, travel).
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn ftr_jaws(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!ftr_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let c = P.f[0].xy;
|
||||
let sn = P.f[0].z;
|
||||
let cs = P.f[0].w;
|
||||
let width = P.f[1].x;
|
||||
let amp = P.f[1].y;
|
||||
let dd = P.f[1].z;
|
||||
let d = ftr_centre(p) - c;
|
||||
let u = d.x * cs + d.y * sn;
|
||||
let v = -d.x * sn + d.y * cs;
|
||||
let ph = fract_euclid(u / width);
|
||||
var f: f32;
|
||||
switch P.u[0].x {
|
||||
case 0u: {
|
||||
f = amp * (1.0 - 4.0 * abs(ph - 0.5));
|
||||
}
|
||||
case 1u: {
|
||||
f = amp * clamp((1.0 - 4.0 * abs(ph - 0.5)) * 2.0, -1.0, 1.0);
|
||||
}
|
||||
case 2u: {
|
||||
f = select(-amp, amp, ph < 0.5);
|
||||
}
|
||||
default: {
|
||||
f = amp * sin(ph * 6.283185307179586);
|
||||
}
|
||||
}
|
||||
var vs = 0.0;
|
||||
var hit = true;
|
||||
if (v + dd < f) {
|
||||
vs = v + dd;
|
||||
} else if (v - dd >= f) {
|
||||
vs = v - dd;
|
||||
} else {
|
||||
hit = false;
|
||||
}
|
||||
var o = vec4<f32>(0.0);
|
||||
if (hit) {
|
||||
o = sample_bilinear(src, u * cs - vs * sn + c.x, u * sn + vs * cs + c.y);
|
||||
}
|
||||
textureStore(out, p, o);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Line Sweep (transition2::line_sweep)
|
||||
// u[0] = (flip, slant ≥ 0); f[0] = (sin, cos, v0, u0); f[1] = (thickness, |slant|, strips,
|
||||
// total travel); f[2].x = completion.
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn ftr_line_sweep(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!ftr_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let f = ftr_centre(p);
|
||||
let sn = P.f[0].x;
|
||||
let cs = P.f[0].y;
|
||||
let u = f.x * cs + f.y * sn;
|
||||
let v = -f.x * sn + f.y * cs;
|
||||
let nstr = P.f[1].z;
|
||||
var k = floor((v - P.f[0].z) / P.f[1].x);
|
||||
if (P.u[0].x != 0u) {
|
||||
k = nstr - 1.0 - k;
|
||||
}
|
||||
let stagger = select(nstr - 1.0 - k, k, P.u[0].y != 0u);
|
||||
let front = P.f[0].w + P.f[2].x * P.f[1].w - P.f[1].y * stagger;
|
||||
textureStore(out, p, select(textureLoad(src, p, 0), vec4<f32>(0.0), u < front));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Twister (transition2::twister)
|
||||
// aux = the backside layer (u[0].x = has one); u[0].y = shading; f[0] = (cx, cy, sin, cos);
|
||||
// f[1] = (half diagonal, completion).
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn ftr_twister(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!ftr_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let c = P.f[0].xy;
|
||||
let sn = P.f[0].z;
|
||||
let cs = P.f[0].w;
|
||||
let d = ftr_centre(p) - c;
|
||||
let u = d.x * cs + d.y * sn;
|
||||
let v = -d.x * sn + d.y * cs;
|
||||
let s01 = clamp((u / P.f[1].x) * 0.5 + 0.5, 0.0, 1.0);
|
||||
let th = 3.141592653589793 * clamp(P.f[1].y * 2.0 - s01, 0.0, 1.0);
|
||||
let k = cos(th);
|
||||
if (abs(k) < 1e-4) {
|
||||
textureStore(out, p, vec4<f32>(0.0));
|
||||
return;
|
||||
}
|
||||
let vs = v / k;
|
||||
var o: vec4<f32>;
|
||||
if (k > 0.0 || P.u[0].x == 0u) {
|
||||
o = sample_bilinear(src, u * cs - vs * sn + c.x, u * sn + vs * cs + c.y);
|
||||
} else {
|
||||
o = sample_bilinear(aux, u * cs + vs * sn + c.x, u * sn - vs * cs + c.y);
|
||||
}
|
||||
if (P.u[0].y != 0u) {
|
||||
let l = 0.4 + 0.6 * abs(k);
|
||||
o = vec4<f32>(o.xyz * l, o.w);
|
||||
}
|
||||
textureStore(out, p, o);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC WarpoMatic (transition2::warpomatic)
|
||||
|
||||
// Reactor "Contrast Differences": src = the plane, aux = its wide mean.
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn ftr_react_contrast(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!ftr_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let a = textureLoad(src, p, 0).x;
|
||||
let b = textureLoad(aux, p, 0).x;
|
||||
textureStore(out, p, vec4<f32>(min(abs(a - b) * 4.0, 1.0), 0.0, 0.0, 0.0));
|
||||
}
|
||||
|
||||
// Reactor "Local Differences": aux = the plane; its gradient magnitude.
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn ftr_react_local(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!ftr_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let g = ftr_grad(p);
|
||||
textureStore(out, p, vec4<f32>(min(sqrt(g.x * g.x + g.y * g.y) * 8.0, 1.0), 0.0, 0.0, 0.0));
|
||||
}
|
||||
|
||||
// aux = the reactor plane (x); data = the layer to reveal (rows). u[0] = (warp direction,
|
||||
// has reveal); f[0] = (completion, blend span, warp amount).
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn ftr_warpo(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!ftr_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let q = ftr_wipe_t(textureLoad(aux, p, 0).x, P.f[0].x, P.f[0].y);
|
||||
let g = ftr_grad(p);
|
||||
let f = ftr_centre(p);
|
||||
let amt = P.f[0].z;
|
||||
let o = g * amt * q;
|
||||
var i = g * amt * (1.0 - q);
|
||||
if (P.u[0].x == 1u) {
|
||||
i = -i;
|
||||
} else if (P.u[0].x == 2u) {
|
||||
i = vec2<f32>(-i.y, i.x);
|
||||
}
|
||||
var a = vec4<f32>(0.0);
|
||||
if (q < 1.0) {
|
||||
a = sample_bilinear(src, f.x + o.x, f.y + o.y);
|
||||
}
|
||||
var inc = vec4<f32>(0.0);
|
||||
if (P.u[0].y != 0u && q > 0.0) {
|
||||
inc = ftr_row_bilinear(f.x + i.x, f.y + i.y);
|
||||
}
|
||||
textureStore(out, p, a + (inc - a) * q);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Radial Shadow (perspective::radial_shadow)
|
||||
// The shadow before its blur. u[0].x = glass edge; f[0] = (light x, y, projection k,
|
||||
// opacity × colour alpha); f[1] = (colour, colour influence).
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn ftr_rshadow(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!ftr_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let l = P.f[0].xy;
|
||||
let q = ftr_centre(p);
|
||||
let s = sample_bilinear(src, l.x + (q.x - l.x) / P.f[0].z, l.y + (q.y - l.y) / P.f[0].z);
|
||||
let sc = ftr_unpremul(s);
|
||||
let sa = max(s.w, 0.0);
|
||||
var a = sa;
|
||||
if (P.u[0].x != 0u) {
|
||||
a = clamp(sa * (1.0 - sa) * 4.0, 0.0, 1.0);
|
||||
}
|
||||
a *= P.f[0].w;
|
||||
let col = P.f[1].xyz;
|
||||
let c = col + (sc - col) * P.f[1].w;
|
||||
textureStore(out, p, vec4<f32>(c * a, a));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Bender (distort3::cc_bender)
|
||||
// u[0].x = style; f[0] = (base x, y, axis x, y); f[1] = (axis length, normal x, y, amount ×
|
||||
// reference).
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn ftr_bender(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!ftr_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let f = ftr_centre(p);
|
||||
let r = f - P.f[0].xy;
|
||||
let t = clamp(dot(r, P.f[0].zw) / P.f[1].x, 0.0, 1.0);
|
||||
let pi = 3.141592653589793;
|
||||
var b: f32;
|
||||
switch P.u[0].x {
|
||||
case 1u: {
|
||||
b = 0.5 * sin(pi * t) * t;
|
||||
}
|
||||
case 2u: {
|
||||
b = 0.5 * t;
|
||||
}
|
||||
case 3u: {
|
||||
b = 0.25 * (1.0 - cos(pi * t)) - 0.1 * sin(2.0 * pi * t);
|
||||
}
|
||||
default: {
|
||||
b = 0.5 * t * t;
|
||||
}
|
||||
}
|
||||
let d = P.f[1].w * b;
|
||||
textureStore(out, p, sample_bilinear(src, f.x - P.f[1].y * d, f.y - P.f[1].z * d));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Blobbylize (distort3::cc_blobbylize)
|
||||
// aux = the blurred blob plane (x). f[0] = (depth, cut away, intensity, ambient);
|
||||
// f[1] = (diffuse, specular, shininess, metal); f[2] = (light x, y, point light height,
|
||||
// point light); f[3] = distant light direction; f[4] = light colour.
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn ftr_blob(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!ftr_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let depth = P.f[0].x;
|
||||
let cut = P.f[0].y;
|
||||
let hv = textureLoad(aux, p, 0).x;
|
||||
let g = ftr_grad(p);
|
||||
let f = ftr_centre(p);
|
||||
let s = sample_bilinear(src, f.x - g.x * depth * 2.0, f.y - g.y * depth * 2.0);
|
||||
let c = ftr_unpremul(s);
|
||||
let a = max(s.w, 0.0);
|
||||
let m = clamp((hv - cut) / max((1.0 - cut) * 0.5, 1e-3), 0.0, 1.0);
|
||||
let alpha = max(a, s.w) * m;
|
||||
if (alpha <= 0.0) {
|
||||
textureStore(out, p, vec4<f32>(0.0));
|
||||
return;
|
||||
}
|
||||
let n = normalize(vec3<f32>(-g.x * depth, -g.y * depth, 1.0));
|
||||
var l = P.f[3].xyz;
|
||||
if (P.f[2].w != 0.0) {
|
||||
l = normalize(vec3<f32>(P.f[2].x - f32(p.x), P.f[2].y - f32(p.y), P.f[2].z));
|
||||
}
|
||||
let intensity = P.f[0].z;
|
||||
let ndl = max(dot(n, l), 0.0);
|
||||
let h3 = normalize(vec3<f32>(l.x, l.y, l.z + 1.0));
|
||||
let sp = powz(max(dot(n, h3), 0.0), P.f[1].z) * P.f[1].y;
|
||||
let lc = P.f[4].xyz;
|
||||
let metal = P.f[1].w;
|
||||
let lit = (P.f[0].w + P.f[1].x * ndl * intensity) * lc;
|
||||
let spec_col = (1.0 - metal) * lc + metal * c * lc;
|
||||
let o = c * lit + (sp * intensity) * spec_col;
|
||||
textureStore(out, p, vec4<f32>(o * alpha, alpha));
|
||||
}
|
||||
|
||||
// 0.5 + atan2(x, z) / 2π, kept on the side of the ±π seam that the sign of `x` puts it on (a
|
||||
// GPU's approximate atan2 can land just across it, which wraps to the opposite edge of the
|
||||
// texture). x = ±0 takes the CPU's −π (its zero is negative there).
|
||||
fn ftr_lon_u(x: f32, z: f32) -> f32 {
|
||||
let u = 0.5 + atan2(x, z) / 6.283185307179586;
|
||||
if (x <= 0.0) {
|
||||
return clamp(u, 0.0, 0.5);
|
||||
}
|
||||
return clamp(u, 0.5, 0.99999994);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Cylinder / CC Sphere (perspective.rs)
|
||||
// Shading (perspective::Shading): f[3] = (light direction, intensity); f[4] = (light colour,
|
||||
// ambient); f[5] = (diffuse, specular, shininess); f[6] = half vector. The layer rectangle in
|
||||
// f[1] = (x0, y0, width, height).
|
||||
|
||||
fn ftr_shade(px: vec4<f32>, n: vec3<f32>) -> vec4<f32> {
|
||||
let c = ftr_unpremul(px);
|
||||
let a = max(px.w, 0.0);
|
||||
if (a <= 0.0) {
|
||||
return vec4<f32>(0.0);
|
||||
}
|
||||
let nl = max(dot(n, P.f[3].xyz), 0.0);
|
||||
var spec = 0.0;
|
||||
if (nl > 0.0) {
|
||||
spec = powz(max(dot(n, P.f[6].xyz), 0.0), P.f[5].z) * P.f[5].y;
|
||||
}
|
||||
let lc = P.f[4].xyz * P.f[3].w;
|
||||
let o = c * (P.f[4].w + P.f[5].x * nl * lc) + spec * lc;
|
||||
return vec4<f32>(o * a, a);
|
||||
}
|
||||
|
||||
// perspective::texel: the layer as a texture at (u, v), wrapping horizontally.
|
||||
fn ftr_texel(u0: f32, v: f32) -> vec4<f32> {
|
||||
let u = fract_euclid(u0);
|
||||
if (v < 0.0 || v > 1.0) {
|
||||
return vec4<f32>(0.0);
|
||||
}
|
||||
let r = P.f[1];
|
||||
return sample_bilinear_clamped(src, r.x + u * r.z, r.y + v * r.w);
|
||||
}
|
||||
|
||||
fn ftr_over(top: vec4<f32>, bottom: vec4<f32>) -> vec4<f32> {
|
||||
return top + bottom * (1.0 - top.w);
|
||||
}
|
||||
|
||||
fn ftr_faces(mode: u32, front: vec4<f32>, back: vec4<f32>) -> vec4<f32> {
|
||||
if (mode == 1u) {
|
||||
return front;
|
||||
}
|
||||
if (mode == 2u) {
|
||||
return back;
|
||||
}
|
||||
return ftr_over(front, back);
|
||||
}
|
||||
|
||||
// Cylinder: u[0].x = render; f[0] = (cx, cy, radius, layer height); f[2].x = rotation.
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn ftr_cylinder(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!ftr_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let d = ftr_centre(p) - P.f[0].xy;
|
||||
let v = d.y / P.f[0].w + 0.5;
|
||||
let s = d.x / P.f[0].z;
|
||||
if (abs(s) >= 1.0) {
|
||||
textureStore(out, p, vec4<f32>(0.0));
|
||||
return;
|
||||
}
|
||||
let tau = 6.283185307179586;
|
||||
let th = asin(s);
|
||||
let cz = cos(th);
|
||||
let spin = P.f[2].x;
|
||||
let front = ftr_shade(ftr_texel((th + spin) / tau + 0.5, v), vec3<f32>(s, 0.0, cz));
|
||||
let back = ftr_shade(ftr_texel((3.141592653589793 - th + spin) / tau + 0.5, v), vec3<f32>(-s, 0.0, cz));
|
||||
textureStore(out, p, ftr_faces(P.u[0].x, front, back));
|
||||
}
|
||||
|
||||
// Sphere: u[0].x = render; f[0] = (cx, cy, radius); f[7..10] = the rotation matrix rows.
|
||||
fn ftr_sphere_tex(q: vec3<f32>) -> vec4<f32> {
|
||||
let m0 = P.f[7].xyz;
|
||||
let m1 = P.f[8].xyz;
|
||||
let m2 = P.f[9].xyz;
|
||||
let t = m0 * q.x + m1 * q.y + m2 * q.z;
|
||||
let lat = asin(clamp(t.y, -1.0, 1.0));
|
||||
return ftr_texel(ftr_lon_u(t.x, t.z), lat / 3.141592653589793 + 0.5);
|
||||
}
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn ftr_sphere(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!ftr_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let sxy = (ftr_centre(p) - P.f[0].xy) / P.f[0].z;
|
||||
let d2 = sxy.x * sxy.x + sxy.y * sxy.y;
|
||||
if (d2 >= 1.0) {
|
||||
textureStore(out, p, vec4<f32>(0.0));
|
||||
return;
|
||||
}
|
||||
let z = sqrt(1.0 - d2);
|
||||
let front = ftr_shade(ftr_sphere_tex(vec3<f32>(sxy, z)), vec3<f32>(sxy, z));
|
||||
let back = ftr_shade(ftr_sphere_tex(vec3<f32>(sxy, -z)), vec3<f32>(-sxy, z));
|
||||
textureStore(out, p, ftr_faces(P.u[0].x, front, back));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Spotlight (perspective::spotlight)
|
||||
// u[0].x = light only; f[0] = (from x, y, height, intensity); f[1] = (axis, inner angle);
|
||||
// f[2] = (colour, cone angle).
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn ftr_spot(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!ftr_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let d = ftr_centre(p) - P.f[0].xy;
|
||||
let v = normalize(vec3<f32>(d, -P.f[0].z));
|
||||
let axis = P.f[1].xyz;
|
||||
// acos(v · axis), well conditioned near the axis.
|
||||
let ang = atan2(length(cross(v, axis)), dot(v, axis));
|
||||
let m = (1.0 - ftr_smoothstep(P.f[1].w, P.f[2].w, ang)) * P.f[0].w;
|
||||
let px = textureLoad(src, p, 0);
|
||||
if (P.u[0].x != 0u) {
|
||||
let a = clamp(m, 0.0, 1.0);
|
||||
textureStore(out, p, vec4<f32>(P.f[2].xyz * a, a));
|
||||
} else {
|
||||
textureStore(out, p, vec4<f32>(px.xyz * (m * P.f[2].xyz), px.w));
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Environment (perspective2::cc_environment)
|
||||
// aux = the relief height map (x); data = the environment image (rows). u[0].x = mapping;
|
||||
// f[0] = (1 / buffer scale, height, environment scale); f[1] = (environment offset, size).
|
||||
|
||||
fn ftr_env_uv(mapping: u32, r: vec3<f32>) -> vec2<f32> {
|
||||
if (mapping == 1u) {
|
||||
let m = 2.0 * max(sqrt(r.x * r.x + r.y * r.y + (r.z + 1.0) * (r.z + 1.0)), 1e-9);
|
||||
return vec2<f32>(0.5 + r.x / m, 0.5 + r.y / m);
|
||||
}
|
||||
if (mapping == 2u) {
|
||||
let ax = abs(r.x);
|
||||
let ay = abs(r.y);
|
||||
let az = abs(r.z);
|
||||
var cr: vec4<f32>;
|
||||
if (ax >= ay && ax >= az) {
|
||||
if (r.x > 0.0) {
|
||||
cr = vec4<f32>(2.0, 1.0, -r.z / ax, r.y / ax);
|
||||
} else {
|
||||
cr = vec4<f32>(0.0, 1.0, r.z / ax, r.y / ax);
|
||||
}
|
||||
} else if (ay >= az) {
|
||||
if (r.y < 0.0) {
|
||||
cr = vec4<f32>(1.0, 0.0, r.x / ay, -r.z / ay);
|
||||
} else {
|
||||
cr = vec4<f32>(1.0, 2.0, r.x / ay, r.z / ay);
|
||||
}
|
||||
} else if (r.z > 0.0) {
|
||||
cr = vec4<f32>(1.0, 1.0, r.x / az, r.y / az);
|
||||
} else {
|
||||
cr = vec4<f32>(1.0, 3.0, r.x / az, -r.y / az);
|
||||
}
|
||||
return vec2<f32>((cr.x + (cr.z + 1.0) * 0.5) / 3.0, (cr.y + (cr.w + 1.0) * 0.5) / 4.0);
|
||||
}
|
||||
let lat = asin(clamp(r.y, -1.0, 1.0));
|
||||
return vec2<f32>(ftr_lon_u(r.x, r.z), 0.5 + lat / 3.141592653589793);
|
||||
}
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn ftr_env(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!ftr_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let a = textureLoad(src, p, 0).w;
|
||||
if (a <= 0.0) {
|
||||
textureStore(out, p, vec4<f32>(0.0));
|
||||
return;
|
||||
}
|
||||
let g = ftr_grad(p) * P.f[0].x;
|
||||
let height = P.f[0].y;
|
||||
let n = normalize(vec3<f32>(-g.x * height, -g.y * height, 1.0));
|
||||
let d = -n.z;
|
||||
let r = vec3<f32>(-2.0 * d * n.x, -2.0 * d * n.y, -(-1.0 - 2.0 * d * n.z));
|
||||
let uv = ftr_env_uv(P.u[0].x, r);
|
||||
let es = P.f[0].z;
|
||||
let ex = fract_euclid(uv.x) * P.f[1].z * es + P.f[1].x;
|
||||
let ey = clamp(uv.y, 0.0, 1.0) * P.f[1].w * es + P.f[1].y;
|
||||
let env = ftr_row_bilinear_clamped(ex, ey);
|
||||
let ec = env.xyz * (1.0 / max(env.w, 1e-3));
|
||||
textureStore(out, p, vec4<f32>(ec * a, a));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 3D Glasses (perspective2::glasses_3d)
|
||||
|
||||
// Translate by f[0].xy (perspective2::shift).
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn ftr_shift(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!ftr_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let f = ftr_centre(p) - P.f[0].xy;
|
||||
textureStore(out, p, sample_bilinear(src, f.x, f.y));
|
||||
}
|
||||
|
||||
// src = left view, aux = right view. u[0].x = 3D view; f[0] = (width, height, balance).
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn ftr_glasses(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!ftr_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let f = ftr_centre(p);
|
||||
let w = P.f[0].x;
|
||||
let h = P.f[0].y;
|
||||
let k = P.f[0].z;
|
||||
let view = P.u[0].x;
|
||||
var o: vec4<f32>;
|
||||
if (view == 0u) {
|
||||
if (f.x < w * 0.5) {
|
||||
o = sample_bilinear(src, f.x * 2.0, f.y);
|
||||
} else {
|
||||
o = sample_bilinear(aux, (f.x - w * 0.5) * 2.0, f.y);
|
||||
}
|
||||
} else if (view == 1u) {
|
||||
if (f.y < h * 0.5) {
|
||||
o = sample_bilinear(src, f.x, f.y * 2.0);
|
||||
} else {
|
||||
o = sample_bilinear(aux, f.x, (f.y - h * 0.5) * 2.0);
|
||||
}
|
||||
} else if (view == 2u) {
|
||||
if (p.y % 2 == 0) {
|
||||
o = textureLoad(src, p, 0);
|
||||
} else {
|
||||
o = textureLoad(aux, p, 0);
|
||||
}
|
||||
} else {
|
||||
let lp = textureLoad(src, p, 0);
|
||||
let rp = textureLoad(aux, p, 0);
|
||||
let lc = ftr_unpremul(lp);
|
||||
let rc = ftr_unpremul(rp);
|
||||
let a = max(max(lp.w, 0.0), max(rp.w, 0.0));
|
||||
let ll = luminance(lc);
|
||||
let rl = luminance(rc);
|
||||
var c: vec3<f32>;
|
||||
switch view {
|
||||
case 3u: {
|
||||
c = vec3<f32>(ll, rl, 0.0);
|
||||
}
|
||||
case 4u: {
|
||||
c = vec3<f32>(ll, 0.0, rl);
|
||||
}
|
||||
case 5u: {
|
||||
c = vec3<f32>(lc.x + (ll - lc.x) * k, rc.y, rc.z);
|
||||
}
|
||||
case 6u: {
|
||||
c = vec3<f32>(ll * (1.0 - 0.5 * k) + lc.x * 0.5 * k, rl, 0.0);
|
||||
}
|
||||
case 7u: {
|
||||
c = vec3<f32>(ll * (1.0 - 0.5 * k) + lc.x * 0.5 * k, 0.0, rl);
|
||||
}
|
||||
default: {
|
||||
c = abs(lc - rc);
|
||||
}
|
||||
}
|
||||
o = vec4<f32>(c * a, a);
|
||||
}
|
||||
textureStore(out, p, o);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// Environment Light Background layers (`three_d::compose::draw_sky`): the layer's
|
||||
// equirectangular image looked up by each pixel's view ray through the camera, composited over
|
||||
// the canvas. src = the canvas, aux = the layer's buffer. f[0] = (forward, zoom); f[1] =
|
||||
// (right, orthographic); f[2] = (down, environment rotation); f[3] = (x offset, y offset,
|
||||
// 1 / scale, opacity) with the offsets ROI / scale − view size / 2; f[4] = (image width, height).
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn adv_sky(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
let dims = out_dims();
|
||||
if (p.x >= dims.x || p.y >= dims.y) {
|
||||
return;
|
||||
}
|
||||
let fwd = P.f[0].xyz;
|
||||
var dir = fwd;
|
||||
if (P.f[1].w == 0.0) {
|
||||
let cx = (f32(p.x) + 0.5) * P.f[3].z + P.f[3].x;
|
||||
let cy = (f32(p.y) + 0.5) * P.f[3].z + P.f[3].y;
|
||||
dir = normalize(fwd * P.f[0].w + P.f[1].xyz * cx + P.f[2].xyz * cy);
|
||||
}
|
||||
// shade::equirect_uv.
|
||||
let pi = 3.141592653589793;
|
||||
var u = 0.5 + (atan2(dir.x, dir.z) + P.f[2].w) / (2.0 * pi);
|
||||
let v = acos(clamp(-dir.y, -1.0, 1.0)) / pi;
|
||||
u = u - floor(u);
|
||||
let iw = P.f[4].x;
|
||||
let ih = P.f[4].y;
|
||||
let s = sample_bilinear(aux, clamp(u * iw, 0.5, iw - 0.5), clamp(v * ih, 0.5, ih - 0.5));
|
||||
let op = P.f[3].w;
|
||||
let d = textureLoad(src, p, 0);
|
||||
textureStore(out, p, s * op + d * (1.0 - s.w * op));
|
||||
}
|
||||
@@ -356,7 +356,7 @@ fn gpu_compositor_draws_advanced_3d_runs_on_the_device() {
|
||||
let (share, max) = off_share(&cpu.data, &gimg.data);
|
||||
eprintln!("walk: {:.3} % beyond 1/255 (max {max:.4})", share * 100.0);
|
||||
assert!(share <= 0.01, "{:.3} % of pixels differ", share * 100.0);
|
||||
// A blend mode in the run takes the CPU's 2D path (its scene still renders on the GPU).
|
||||
// A blend mode in the run takes the 2D compositing path (`SplitRun`, also on the GPU).
|
||||
p.comp_mut(cid).unwrap().layers.iter_mut().find(|l| l.switches.three_d && !l.is_light() && l.source.is_av()).unwrap().blend_mode =
|
||||
effectcraft_color::BlendMode::Screen;
|
||||
let (ctx, run) = run_of(&p, cid);
|
||||
@@ -440,3 +440,90 @@ fn gpu_environment_light_on_flat_extrusions_has_no_nan() {
|
||||
assert_eq!(nan, 0, "non-finite pixels");
|
||||
compare(&s, &g);
|
||||
}
|
||||
|
||||
/// Advanced 3D runs whose layers take the 2D compositing path (blend modes, a 2D and a 3D track
|
||||
/// matte, Preserve Transparency) and environment backgrounds composite on the GPU: against the
|
||||
/// CPU compositor on the same (GPU) rasters they agree to float rounding; against the CPU
|
||||
/// rasteriser within the raster tolerance.
|
||||
#[test]
|
||||
fn gpu_compositor_draws_split_advanced_3d_runs_and_skies() {
|
||||
let Some(g) = gpu() else { return };
|
||||
for case in 0..10 {
|
||||
let (mut p, cid) = scene_project();
|
||||
if case >= 4 {
|
||||
p.settings.bit_depth = effectcraft_project::BitDepth::Bpc32;
|
||||
}
|
||||
let classic = case >= 8;
|
||||
let case = if classic { 3 } else { case % 4 };
|
||||
if classic {
|
||||
// Classic 3D: the sky on the GPU too, then the run's planes.
|
||||
p.comp_mut(cid).unwrap().renderer = R3::Classic3D;
|
||||
}
|
||||
let bg = p.add_item("B", Default::default(), None, ItemKind::Solid(Solid { color: [0.1, 0.1, 0.15], width: 240, height: 160, pixel_aspect: 1.0 }));
|
||||
let comp = p.comp(cid).unwrap().clone();
|
||||
let l = build::layer(&mut p, &comp, "B", LayerSource::Solid { item: bg }, (240, 160), None);
|
||||
p.comp_mut(cid).unwrap().layers.push(l);
|
||||
// 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 ramp = effectcraft_effects::instantiate(spec, &mut build::Ids(&mut next), spec.name, [32.0, 16.0]);
|
||||
p.next_id = next;
|
||||
if case == 3 {
|
||||
// A 2D track matte layer above the run.
|
||||
let sid = p.add_item("M", Default::default(), None, ItemKind::Solid(Solid { color: [1.0, 1.0, 1.0], width: 120, height: 90, pixel_aspect: 1.0 }));
|
||||
add(&mut p, cid, LayerSource::Solid { item: sid }, |m| {
|
||||
m.switches.video = false;
|
||||
set(m, "transform/rotation", Value::Scalar(20.0));
|
||||
});
|
||||
}
|
||||
let c = p.comp_mut(cid).unwrap();
|
||||
let env = c.layers.iter_mut().find(|l| l.environment).unwrap();
|
||||
env.props.sub_mut("effects").unwrap().children.push(ramp.into());
|
||||
let ids: Vec<_> = c.layers.iter().filter(|l| l.switches.three_d && !l.is_light() && !l.environment && l.source.is_av()).map(|l| l.id).collect();
|
||||
let top = c.layers[0].id;
|
||||
fn by_id(c: &mut Comp, id: effectcraft_project::LayerId) -> &mut effectcraft_project::Layer {
|
||||
c.layers.iter_mut().find(|l| l.id == id).unwrap()
|
||||
}
|
||||
match case {
|
||||
0 => by_id(c, ids[1]).blend_mode = effectcraft_color::BlendMode::Screen,
|
||||
1 => {
|
||||
// The card is a 3D track matte (luma) for the sphere; the torus multiplies.
|
||||
let card = ids[0];
|
||||
by_id(c, ids[1]).blend_mode = effectcraft_color::BlendMode::Multiply;
|
||||
by_id(c, ids[2]).track_matte = Some(effectcraft_project::TrackMatte { layer: card, kind: effectcraft_project::MatteKind::Luma });
|
||||
by_id(c, ids[0]).switches.video = false;
|
||||
}
|
||||
2 => {
|
||||
// Preserve Transparency and an Overlay layer.
|
||||
by_id(c, ids[0]).preserve_transparency = true;
|
||||
by_id(c, ids[1]).blend_mode = effectcraft_color::BlendMode::Overlay;
|
||||
}
|
||||
_ => {
|
||||
// An environment background behind the run, and the 2D track matte.
|
||||
c.layers.iter_mut().find(|l| l.environment).unwrap().environment_background = true;
|
||||
by_id(c, ids[0]).track_matte = Some(effectcraft_project::TrackMatte { layer: top, kind: effectcraft_project::MatteKind::Alpha });
|
||||
}
|
||||
}
|
||||
let (ctx, run) = run_of(&p, cid);
|
||||
let r0 = Renderer::new(&p, &NoFootage, RenderOpts::default());
|
||||
if case < 3 {
|
||||
assert!(r0.split_adv_run(&ctx, &run, (240, 160)).is_some(), "case {case}: split");
|
||||
}
|
||||
let cpu = r0.comp_frame(cid, Tick::ZERO);
|
||||
let mut r = Renderer::new(&p, &NoFootage, RenderOpts { backend: effectcraft_render::Backend::Gpu, ..Default::default() });
|
||||
r.accel = Some(&g);
|
||||
// The CPU compositor on the GPU's rasters.
|
||||
let hybrid = r.comp_frame_cpu(cid, Tick::ZERO);
|
||||
let before = g.context().transfer_stats().readbacks;
|
||||
let gimg = g.render(&r, cid, Tick::ZERO).expect("gpu walk");
|
||||
// On the device end to end: the frame's own readback only (no CPU fallback).
|
||||
assert_eq!(g.context().transfer_stats().readbacks - before, 1, "case {case}: readbacks");
|
||||
let (share, max) = off_share(&hybrid.data, &gimg.data);
|
||||
eprintln!("split case {case}: gpu walk vs cpu compositing {:.4} % beyond 1/255 (max {max:.4})", share * 100.0);
|
||||
assert!(share <= 0.001, "case {case}: {:.3} % of pixels differ (max {max})", share * 100.0);
|
||||
let (share, max) = off_share(&cpu.data, &gimg.data);
|
||||
eprintln!("split case {case}: gpu vs cpu {:.3} % beyond 1/255 (max {max:.4})", share * 100.0);
|
||||
assert!(share <= 0.015, "case {case}: {:.3} % of pixels differ from the CPU", share * 100.0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
//! GPU CC light effects (CC Light Rays, CC Light Burst 2.5, CC Light Sweep, CC Light Wipe) vs
|
||||
//! the CPU effects (the oracle): direct on a buffer at full and half resolution, as
|
||||
//! adjustment, and composited at 8 and 32 bpc.
|
||||
|
||||
use effectcraft_keyframe::Value;
|
||||
|
||||
use crate::tests::{c, effect_case, n};
|
||||
|
||||
fn e(v: u32) -> Value {
|
||||
Value::Enum(v)
|
||||
}
|
||||
|
||||
fn on() -> Value {
|
||||
Value::Bool(true)
|
||||
}
|
||||
|
||||
fn pt(x: f64, y: f64) -> Value {
|
||||
Value::Vec2([x, y])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn light_rays() {
|
||||
effect_case("ec.generate.cclightrays", &[("center", pt(30.0, 20.0))]);
|
||||
effect_case(
|
||||
"ec.generate.cclightrays",
|
||||
&[("center", pt(40.0, 18.0)), ("intensity", n(250.0)), ("radius", n(15.0)), ("shape", e(1)), ("transferMode", e(1))],
|
||||
);
|
||||
effect_case(
|
||||
"ec.generate.cclightrays",
|
||||
&[("intensity", n(80.0)), ("colorFromSource", Value::Bool(false)), ("color", c(1.0, 0.6, 0.2)), ("transferMode", e(2)), ("warpSoftness", n(120.0))],
|
||||
);
|
||||
effect_case("ec.generate.cclightrays", &[("intensity", n(150.0)), ("transferMode", e(3)), ("radius", n(25.0))]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn light_burst() {
|
||||
effect_case("ec.generate.cclightburst", &[("center", pt(30.0, 20.0)), ("burst", e(1))]);
|
||||
effect_case("ec.generate.cclightburst", &[("rayLength", n(80.0)), ("burst", e(2)), ("intensity", n(150.0))]);
|
||||
effect_case("ec.generate.cclightburst", &[("rayLength", n(30.0)), ("setColor", on()), ("color", c(0.3, 0.7, 1.0)), ("burst", e(1))]);
|
||||
effect_case("ec.generate.cclightburst", &[("rayLength", n(40.0))]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn light_sweep() {
|
||||
effect_case("ec.generate.cclightsweep", &[("center", pt(30.0, 20.0))]);
|
||||
effect_case("ec.generate.cclightsweep", &[("shape", e(0)), ("width", n(30.0)), ("edgeThickness", n(4.0)), ("lightReceptionMode", e(1))]);
|
||||
effect_case("ec.generate.cclightsweep", &[("shape", e(2)), ("direction", n(60.0)), ("lightReceptionMode", e(2)), ("lightColor", c(1.0, 0.8, 0.4))]);
|
||||
effect_case("ec.generate.cclightsweep", &[("edgeThickness", n(0.0)), ("sweepIntensity", n(120.0))]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn light_wipe() {
|
||||
effect_case("ec.transition.cclightwipe", &[("completion", n(40.0)), ("center", pt(30.0, 20.0))]);
|
||||
effect_case("ec.transition.cclightwipe", &[("completion", n(25.0)), ("shape", e(0)), ("direction", n(30.0)), ("colorFromSource", on())]);
|
||||
effect_case("ec.transition.cclightwipe", &[("completion", n(60.0)), ("shape", e(2)), ("reverse", on()), ("color", c(0.2, 0.9, 0.5))]);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
//! GPU Numbers and Timecode vs the CPU effects (the oracle): direct on a buffer at full and half
|
||||
//! resolution, as adjustment, and composited at 8 and 32 bpc.
|
||||
|
||||
use effectcraft_keyframe::Value;
|
||||
|
||||
use crate::tests::{c, effect_case, n};
|
||||
|
||||
fn e(v: u32) -> Value {
|
||||
Value::Enum(v)
|
||||
}
|
||||
|
||||
fn pt(x: f64, y: f64) -> Value {
|
||||
Value::Vec2([x, y])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn numbers() {
|
||||
effect_case("ec.text.numbers", &[("format/value", n(42.5)), ("size", n(14.0))]);
|
||||
effect_case(
|
||||
"ec.text.numbers",
|
||||
&[("format/value", n(-7.0)), ("size", n(12.0)), ("fillAndStroke/displayOptions", e(2)), ("fillAndStroke/strokeWidth", n(1.5)), ("tracking", n(2.0))],
|
||||
);
|
||||
effect_case(
|
||||
"ec.text.numbers",
|
||||
&[
|
||||
("format/value", n(3.25)),
|
||||
("size", n(16.0)),
|
||||
("fillAndStroke/displayOptions", e(3)),
|
||||
("fillAndStroke/fillColor", c(0.2, 0.6, 1.0)),
|
||||
("compositeOnOriginal", Value::Bool(false)),
|
||||
],
|
||||
);
|
||||
effect_case("ec.text.numbers", &[("size", n(10.0)), ("fillAndStroke/displayOptions", e(1)), ("fillAndStroke/position", pt(20.0, 15.0))]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn timecode() {
|
||||
effect_case("ec.text.timecode", &[("textSize", n(10.0)), ("textPosition", pt(35.0, 22.0))]);
|
||||
effect_case(
|
||||
"ec.text.timecode",
|
||||
&[("textSize", n(9.0)), ("displayFormat", e(1)), ("renderOnOriginal", Value::Bool(false)), ("textColor", c(1.0, 0.8, 0.1))],
|
||||
);
|
||||
effect_case("ec.text.timecode", &[("textSize", n(8.0)), ("showBox", Value::Bool(true)), ("boxColor", c(0.1, 0.1, 0.4)), ("opacity", n(70.0))]);
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
//! GPU time effects (Time Difference, Time Displacement, CC Force Motion Blur, CC Wide Time,
|
||||
//! Pixel Motion Blur, Timewarp) vs the CPU effects (the oracle). They fetch other frames through
|
||||
//! the effect host: a precomp whose content moves, at a nonzero time, composited at 8 and
|
||||
//! 32 bpc (the direct cases have no host and pass the layer through).
|
||||
|
||||
use effectcraft_color::Label;
|
||||
use effectcraft_keyframe::{Keyframe, Value};
|
||||
use effectcraft_project::{BitDepth, Comp, ItemKind, LayerSource, build};
|
||||
use effectcraft_render::RenderOpts;
|
||||
use effectcraft_time::{FrameRate, Tick};
|
||||
|
||||
use crate::tests::{Scene, check, compare_at, effect_case, n, opts, set, v3};
|
||||
|
||||
fn e(v: u32) -> Value {
|
||||
Value::Enum(v)
|
||||
}
|
||||
|
||||
fn on() -> Value {
|
||||
Value::Bool(true)
|
||||
}
|
||||
|
||||
/// `id` with `vals` on a moving precomp; `map` adds a hidden footage layer and sets parameter
|
||||
/// `map` to it.
|
||||
fn moving(id: &str, vals: &[(&str, Value)], map: Option<&str>) {
|
||||
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 vals = vals.to_vec();
|
||||
if let Some(param) = map {
|
||||
let mut m = s.footage(50, 36);
|
||||
m.switches.video = false;
|
||||
let mid = s.push(m);
|
||||
vals.push((param, Value::Layer(Some(mid.0))));
|
||||
}
|
||||
let inner = Comp::new(60, 40, FrameRate::FPS_30, Tick::from_seconds_f64(2.0));
|
||||
let inner_id = s.p.add_item("Inner", Label::Sandstone, None, ItemKind::Comp(inner.into()));
|
||||
let mut a = s.footage(30, 24);
|
||||
let pos = a.props.prop_mut("transform/position").unwrap();
|
||||
pos.keys = vec![Keyframe::new(Tick::ZERO, v3(10.0, 12.0)), Keyframe::new(Tick::from_seconds_f64(1.0), v3(50.0, 30.0))];
|
||||
let rot = a.props.prop_mut("transform/rotation").unwrap();
|
||||
rot.keys = vec![Keyframe::new(Tick::ZERO, Value::Scalar(0.0)), Keyframe::new(Tick::from_seconds_f64(1.0), Value::Scalar(90.0))];
|
||||
s.p.comp_mut(inner_id).unwrap().layers = vec![a];
|
||||
let mut l = build::layer(&mut s.p, &s.comp, "Inner", LayerSource::Comp { item: inner_id }, (60, 40), None);
|
||||
s.effect(&mut l, id, &vals);
|
||||
set(&mut l, "transform/rotation", Value::Scalar(8.0));
|
||||
s.push(l);
|
||||
let t = Tick::from_seconds_f64(0.4);
|
||||
let label = format!("{id} {vals:?} {depth:?}");
|
||||
check(&label, compare_at(&s, opts(), t), 0.0);
|
||||
check(&format!("{label} half"), compare_at(&s, RenderOpts { scale: 0.5, ..opts() }, t), 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn without_a_host() {
|
||||
for id in
|
||||
["ec.time.timedifference", "ec.time.timedisplacement", "ec.time.ccforcemotionblur", "ec.time.ccwidetime", "ec.time.pixelmotionblur", "ec.time.timewarp"]
|
||||
{
|
||||
effect_case(id, &[]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frame_averages() {
|
||||
moving("ec.time.ccforcemotionblur", &[], None);
|
||||
moving("ec.time.ccforcemotionblur", &[("motionBlurLevels", n(5.0)), ("shutterAngle", n(400.0))], None);
|
||||
moving("ec.time.ccwidetime", &[], None);
|
||||
moving("ec.time.ccwidetime", &[("forwardSteps", n(1.0)), ("backwardSteps", n(4.0))], None);
|
||||
moving("ec.time.pixelmotionblur", &[], None);
|
||||
moving("ec.time.pixelmotionblur", &[("shutterControl", e(1)), ("shutterAngle", n(300.0)), ("shutterSamples", n(5.0))], None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn time_difference() {
|
||||
moving("ec.time.timedifference", &[("timeOffset", n(-0.1))], None);
|
||||
for alpha in 0..9 {
|
||||
moving(
|
||||
"ec.time.timedifference",
|
||||
&[("timeOffset", n(0.2)), ("contrast", n(40.0)), ("absoluteDifference", Value::Bool(alpha % 2 == 0)), ("alphaChannel", e(alpha))],
|
||||
None,
|
||||
);
|
||||
}
|
||||
moving("ec.time.timedifference", &[("timeOffset", n(0.1)), ("alphaChannel", e(2))], Some("targetLayer"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn time_displacement() {
|
||||
moving("ec.time.timedisplacement", &[("maxDisplacementTime", n(0.3)), ("timeResolution", n(10.0))], None);
|
||||
moving("ec.time.timedisplacement", &[("maxDisplacementTime", n(0.5)), ("timeResolution", n(30.0))], Some("displacementMapLayer"));
|
||||
moving("ec.time.timedisplacement", &[("maxDisplacementTime", n(2.0)), ("stretchMap", Value::Bool(false))], Some("displacementMapLayer"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn timewarp() {
|
||||
for method in 0..3 {
|
||||
moving("ec.time.timewarp", &[("method", e(method)), ("speed", n(37.0))], None);
|
||||
}
|
||||
moving("ec.time.timewarp", &[("method", e(1)), ("adjustTimeBy", e(1)), ("sourceFrame", n(7.4))], None);
|
||||
moving("ec.time.timewarp", &[("method", e(2)), ("speed", n(43.0)), ("motionBlur/enableMotionBlur", on()), ("motionBlur/shutterSamples", n(3.0))], None);
|
||||
moving(
|
||||
"ec.time.timewarp",
|
||||
&[
|
||||
("speed", n(61.0)),
|
||||
("tuning/filtering", e(1)),
|
||||
("tuning/errorThreshold", n(30.0)),
|
||||
("sourceCrops/leftCrop", n(4.0)),
|
||||
("sourceCrops/topCrop", n(3.0)),
|
||||
],
|
||||
None,
|
||||
);
|
||||
moving("ec.time.timewarp", &[("speed", n(55.0)), ("tuning/buildFromOneImage", on())], None);
|
||||
moving("ec.time.timewarp", &[("speed", n(47.0))], Some("warpLayer"));
|
||||
// A Matte Layer renders on the CPU (catalog::gpu_supported).
|
||||
moving("ec.time.timewarp", &[("speed", n(47.0)), ("show", e(0))], Some("matteLayer"));
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
//! GPU transition and perspective effects (Block Dissolve, the CC transitions, Radial Shadow,
|
||||
//! CC Bender, CC Blobbylize, CC Cylinder, CC Sphere, CC Spotlight, CC Environment, 3D Glasses)
|
||||
//! vs the CPU effects (the oracle): direct on a buffer at full and half resolution, as
|
||||
//! adjustment, and composited at 8 and 32 bpc; effects reading other layers also with a layer
|
||||
//! chosen.
|
||||
|
||||
use effectcraft_keyframe::Value;
|
||||
use effectcraft_project::BitDepth;
|
||||
use effectcraft_render::RenderOpts;
|
||||
use effectcraft_time::Tick;
|
||||
|
||||
use crate::tests::{Scene, c, check, compare_at, effect_case, n, opts, set};
|
||||
|
||||
fn e(v: u32) -> Value {
|
||||
Value::Enum(v)
|
||||
}
|
||||
|
||||
fn on() -> Value {
|
||||
Value::Bool(true)
|
||||
}
|
||||
|
||||
fn pt(x: f64, y: f64) -> Value {
|
||||
Value::Vec2([x, y])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_dissolve() {
|
||||
effect_case("ec.transition.blockdissolve", &[("completion", n(40.0))]);
|
||||
effect_case("ec.transition.blockdissolve", &[("completion", n(55.0)), ("blockWidth", n(7.0)), ("blockHeight", n(5.5)), ("feather", n(3.0))]);
|
||||
effect_case("ec.transition.blockdissolve", &[("completion", n(30.0)), ("blockWidth", n(4.3)), ("blockHeight", n(9.0)), ("softEdges", Value::Bool(false))]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grid_and_scale_wipes() {
|
||||
effect_case("ec.transition.ccgridwipe", &[("completion", n(40.0)), ("center", pt(33.0, 21.0))]);
|
||||
effect_case("ec.transition.ccgridwipe", &[("completion", n(60.0)), ("rotation", n(20.0)), ("border", n(3.0)), ("tiles", n(7.0)), ("shape", e(0))]);
|
||||
effect_case("ec.transition.ccgridwipe", &[("completion", n(35.0)), ("shape", e(2)), ("reverse", on()), ("tiles", n(5.0))]);
|
||||
effect_case("ec.transition.ccradialscalewipe", &[("completion", n(40.0)), ("center", pt(30.0, 20.0))]);
|
||||
effect_case("ec.transition.ccradialscalewipe", &[("completion", n(30.0)), ("center", pt(40.0, 25.0)), ("reverse", on())]);
|
||||
effect_case("ec.transition.ccscalewipe", &[("stretch", n(2.0)), ("center", pt(30.0, 20.0))]);
|
||||
effect_case("ec.transition.ccscalewipe", &[("stretch", n(0.7)), ("direction", n(200.0))]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gradient_driven_wipes() {
|
||||
effect_case("ec.transition.ccglasswipe", &[("completion", n(45.0))]);
|
||||
effect_case("ec.transition.ccglasswipe", &[("completion", n(30.0)), ("softness", n(40.0)), ("displacementAmount", n(25.0))]);
|
||||
effect_case("ec.transition.ccimagewipe", &[("completion", n(50.0))]);
|
||||
effect_case(
|
||||
"ec.transition.ccimagewipe",
|
||||
&[("completion", n(40.0)), ("property", e(5)), ("blur", n(4.0)), ("inverseGradient", on()), ("borderSoftness", n(20.0))],
|
||||
);
|
||||
effect_case("ec.transition.ccimagewipe", &[("completion", n(60.0)), ("property", e(6)), ("autoSoftness", Value::Bool(false))]);
|
||||
effect_case("ec.transition.ccwarpomatic", &[("completion", n(40.0))]);
|
||||
effect_case("ec.transition.ccwarpomatic", &[("completion", n(55.0)), ("reactor", e(1)), ("warpDirection", e(1))]);
|
||||
effect_case("ec.transition.ccwarpomatic", &[("completion", n(35.0)), ("reactor", e(2)), ("warpDirection", e(2)), ("blendSpan", n(30.0))]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn geometric_transitions() {
|
||||
effect_case("ec.transition.ccjaws", &[("completion", n(30.0)), ("center", pt(33.0, 21.0))]);
|
||||
for shape in 1..4 {
|
||||
effect_case("ec.transition.ccjaws", &[("completion", n(20.0)), ("shape", e(shape)), ("direction", n(25.0)), ("width", n(13.0))]);
|
||||
}
|
||||
effect_case("ec.transition.cclinesweep", &[("completion", n(40.0))]);
|
||||
effect_case(
|
||||
"ec.transition.cclinesweep",
|
||||
&[("completion", n(50.0)), ("direction", n(35.0)), ("slant", n(-12.0)), ("thickness", n(7.0)), ("flipDirection", on())],
|
||||
);
|
||||
effect_case("ec.transition.cctwister", &[("completion", n(30.0)), ("center", pt(33.0, 21.0))]);
|
||||
effect_case("ec.transition.cctwister", &[("completion", n(60.0)), ("axis", n(10.0)), ("shading", Value::Bool(false))]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn perspective() {
|
||||
effect_case("ec.perspective.radialshadow", &[("lightSource", pt(20.0, 10.0)), ("projectionDistance", n(15.0))]);
|
||||
effect_case(
|
||||
"ec.perspective.radialshadow",
|
||||
&[
|
||||
("lightSource", pt(50.0, 15.0)),
|
||||
("softness", n(6.0)),
|
||||
("render", e(1)),
|
||||
("colorInfluence", n(60.0)),
|
||||
("resizeLayer", on()),
|
||||
("color", c(0.3, 0.0, 0.5)),
|
||||
],
|
||||
);
|
||||
effect_case("ec.perspective.radialshadow", &[("shadowOnly", on()), ("opacity", n(80.0)), ("resizeLayer", on())]);
|
||||
effect_case("ec.perspective.cccylinder", &[("position", pt(35.0, 22.0)), ("rotation", n(30.0))]);
|
||||
effect_case("ec.perspective.cccylinder", &[("radius", n(60.0)), ("render", e(1)), ("lightDirection", n(70.0)), ("roughness", n(0.2))]);
|
||||
effect_case("ec.perspective.cccylinder", &[("render", e(2)), ("lightHeight", n(20.0)), ("specular", n(60.0))]);
|
||||
effect_case("ec.perspective.ccsphere", &[("offset", pt(35.0, 22.0)), ("radius", n(18.0)), ("rotationX", n(20.0)), ("rotationY", n(35.0))]);
|
||||
effect_case("ec.perspective.ccsphere", &[("radius", n(20.0)), ("rotationZ", n(50.0)), ("render", e(2))]);
|
||||
effect_case("ec.perspective.ccspotlight", &[("from", pt(15.0, 8.0)), ("to", pt(35.0, 22.0))]);
|
||||
effect_case(
|
||||
"ec.perspective.ccspotlight",
|
||||
&[("from", pt(50.0, 5.0)), ("to", pt(30.0, 30.0)), ("coneAngle", n(35.0)), ("edgeSoftness", n(60.0)), ("render", e(1))],
|
||||
);
|
||||
effect_case("ec.perspective.ccspotlight", &[("to", pt(35.0, 22.0)), ("edgeSoftness", n(0.0)), ("color", c(1.0, 0.7, 0.3)), ("intensity", n(150.0))]);
|
||||
for mapping in 0..3 {
|
||||
effect_case("ec.perspective.ccenvironment", &[("mapping", e(mapping)), ("height", n(40.0))]);
|
||||
}
|
||||
effect_case("ec.perspective.ccenvironment", &[("filterEnvironment", Value::Bool(false))]);
|
||||
for view in 0..9 {
|
||||
effect_case("ec.perspective.3dglasses", &[("view3d", e(view)), ("sceneConvergence", n(4.0)), ("verticalAlignment", n(1.5))]);
|
||||
}
|
||||
effect_case("ec.perspective.3dglasses", &[("units", e(1)), ("sceneConvergence", n(3.0)), ("leftRightSwap", on()), ("balance", n(12.0))]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn distort() {
|
||||
effect_case("ec.distort.ccbender", &[("amount", n(40.0)), ("top", pt(35.0, 0.0)), ("base", pt(35.0, 44.0))]);
|
||||
for style in 1..4 {
|
||||
effect_case(
|
||||
"ec.distort.ccbender",
|
||||
&[("amount", n(-30.0)), ("style", e(style)), ("top", pt(20.0, 5.0)), ("base", pt(40.0, 40.0)), ("adjustToDistance", on())],
|
||||
);
|
||||
}
|
||||
effect_case("ec.distort.ccblobbylize", &[]);
|
||||
effect_case(
|
||||
"ec.distort.ccblobbylize",
|
||||
&[("property", e(4)), ("softness", n(3.0)), ("cutAway", n(30.0)), ("lightType", e(1)), ("lightPosition", pt(20.0, 10.0))],
|
||||
);
|
||||
effect_case("ec.distort.ccblobbylize", &[("property", e(5)), ("metal", n(30.0)), ("roughness", n(0.2))]);
|
||||
}
|
||||
|
||||
/// Effects reading another layer (gradient, reveal, backside, environment, stereo views).
|
||||
#[test]
|
||||
fn with_other_layers() {
|
||||
for depth in [BitDepth::Bpc8, BitDepth::Bpc32] {
|
||||
let cases: Vec<(&str, Vec<(&str, Value)>)> = vec![
|
||||
("ec.transition.ccglasswipe", vec![("completion", n(40.0)), ("gradientLayer", Value::Layer(None)), ("layerToReveal", Value::Layer(None))]),
|
||||
("ec.transition.ccimagewipe", vec![("completion", n(45.0)), ("layer", Value::Layer(None))]),
|
||||
("ec.transition.ccwarpomatic", vec![("completion", n(40.0)), ("reactorLayer", Value::Layer(None)), ("layerToReveal", Value::Layer(None))]),
|
||||
("ec.transition.cctwister", vec![("completion", n(70.0)), ("backside", Value::Layer(None))]),
|
||||
("ec.perspective.ccenvironment", vec![("environment", Value::Layer(None))]),
|
||||
("ec.perspective.3dglasses", vec![("view3d", e(5)), ("leftView", Value::Layer(None)), ("sceneConvergence", n(3.0))]),
|
||||
("ec.distort.ccblobbylize", vec![("blobLayer", Value::Layer(None))]),
|
||||
];
|
||||
for (id, vals) in cases {
|
||||
let mut s = Scene::new(depth);
|
||||
let bg = s.solid([0.15, 0.1, 0.2], 97, 61);
|
||||
s.push(bg);
|
||||
let mut other = s.footage(50, 36);
|
||||
other.switches.video = false;
|
||||
let oid = s.push(other);
|
||||
let vals: Vec<(&str, Value)> = vals.into_iter().map(|(k, v)| (k, if v == Value::Layer(None) { Value::Layer(Some(oid.0)) } else { v })).collect();
|
||||
let mut l = s.footage(70, 44);
|
||||
s.effect(&mut l, id, &vals);
|
||||
set(&mut l, "transform/rotation", Value::Scalar(8.0));
|
||||
s.push(l);
|
||||
let label = format!("{id} with a layer {depth:?}");
|
||||
check(&label, compare_at(&s, opts(), Tick::ZERO), 0.0);
|
||||
check(&format!("{label} half"), compare_at(&s, RenderOpts { scale: 0.5, ..opts() }, Tick::ZERO), 0.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
+93
-12
@@ -1,9 +1,9 @@
|
||||
//! The per-frame compositing walk on the GPU: the same steps as `Renderer::draw_comp` (layer
|
||||
//! order, 3D runs, collapsed precomps, track mattes, Preserve Transparency, layer styles, motion
|
||||
//! blur, bit-depth quantisation, colour conversions), with layer pixels taken from the CPU
|
||||
//! renderer and its layer cache. Classic 3D runs and adjustment layers run on the GPU; steps
|
||||
//! without a GPU implementation (Advanced 3D runs, wireframes, anything a kernel declines) read
|
||||
//! the canvas back, run on the CPU and upload the result.
|
||||
//! renderer and its layer cache. Classic and Advanced 3D runs (environment skies included) and
|
||||
//! adjustment layers run on the GPU; steps without a GPU implementation (anything a kernel
|
||||
//! declines) read the canvas back, run on the CPU and upload the result.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -99,6 +99,17 @@ fn draw_comp<'a>(e: &mut Enc, r: &Renderer<'a>, ctx: &EvalCtx<'a>, canvas: &mut
|
||||
/// layers splitting the run as on the CPU; Advanced 3D, wireframes and runs the kernel cannot
|
||||
/// hold draw on the CPU.
|
||||
fn draw_3d<'a>(e: &mut Enc, r: &Renderer<'a>, ctx: &EvalCtx<'a>, run: &[&'a Layer], canvas: &mut GpuImage) -> Option<()> {
|
||||
// Environment Light Background layers: the sky behind the rest of the run (drawn first, as
|
||||
// `three_d::compose::draw_run` does).
|
||||
if run.iter().any(|l| l.environment_background) {
|
||||
for l in run.iter().filter(|l| l.environment_background) {
|
||||
if let Some(sky) = r.sky_draw(ctx, l) {
|
||||
*canvas = crate::adv3d::sky(e, &sky, canvas)?;
|
||||
}
|
||||
}
|
||||
let rest: Vec<&'a Layer> = run.iter().copied().filter(|l| !l.environment_background).collect();
|
||||
return draw_3d(e, r, ctx, &rest, canvas);
|
||||
}
|
||||
if let Some(k) = run.iter().position(|l| l.switches.adjustment) {
|
||||
draw_3d(e, r, ctx, &run[..k], canvas)?;
|
||||
draw_layer(e, r, ctx, run[k], canvas)?;
|
||||
@@ -115,6 +126,15 @@ fn draw_3d<'a>(e: &mut Enc, r: &Renderer<'a>, ctx: &EvalCtx<'a>, run: &[&'a Laye
|
||||
}
|
||||
return Some(());
|
||||
}
|
||||
// Advanced 3D with layers on the 2D compositing path (blend modes, track mattes, Preserve
|
||||
// Transparency).
|
||||
if let Some(split) = r.split_adv_run(ctx, run, (canvas.width, canvas.height)) {
|
||||
match draw_split(e, r, ctx, &split, canvas) {
|
||||
Some(img) => *canvas = img,
|
||||
None => on_cpu(e, canvas, |img| r.draw_3d_run(ctx, run, img))?,
|
||||
}
|
||||
return Some(());
|
||||
}
|
||||
match r.prepare_3d_run(ctx, run, (canvas.width, canvas.height)).and_then(|prep| crate::classic3d::draw_run(e, &prep, canvas)) {
|
||||
Some(img) => *canvas = img,
|
||||
None => on_cpu(e, canvas, |img| r.draw_3d_run(ctx, run, img))?,
|
||||
@@ -122,6 +142,44 @@ fn draw_3d<'a>(e: &mut Enc, r: &Renderer<'a>, ctx: &EvalCtx<'a>, run: &[&'a Laye
|
||||
Some(())
|
||||
}
|
||||
|
||||
/// `three_d::adv::draw_run`'s split path on the GPU canvas: the plain layers as one scene over
|
||||
/// the canvas, then each special layer (far to near) rendered alone, hidden where the main
|
||||
/// scene is nearer, matted (a 3D matte through the camera, else the 2D placement), Preserve
|
||||
/// Transparency and blended. `None` = render the run on the CPU.
|
||||
fn draw_split(e: &mut Enc, r: &Renderer, ctx: &EvalCtx, split: &effectcraft_render::three_d::adv::SplitRun, canvas: &GpuImage) -> Option<GpuImage> {
|
||||
let (w, h) = (canvas.width, canvas.height);
|
||||
let main_prep = split.main();
|
||||
if main_prep.out != (w, h) {
|
||||
return None;
|
||||
}
|
||||
let encode = !main_prep.linear;
|
||||
let main = crate::adv3d::resolve_run(e, &main_prep)?;
|
||||
let mut cv = match &main {
|
||||
Some(m) => crate::adv3d::finish(e, m, encode, Some(canvas)),
|
||||
None => canvas.clone(),
|
||||
};
|
||||
for i in 0..split.len() {
|
||||
let Some(iso) = crate::adv3d::resolve_run(e, &split.special(i))? else { continue };
|
||||
let iso = match &main {
|
||||
Some(m) => crate::adv3d::occluded(e, &iso, m, encode),
|
||||
None => crate::adv3d::finish(e, &iso, encode, None),
|
||||
};
|
||||
let matte3d = split.with_matte3d(i, |p| match p {
|
||||
None => Some(e.zeros(w, h)),
|
||||
Some(p) => Some(match crate::adv3d::resolve_run(e, p)? {
|
||||
Some(m) => crate::adv3d::finish(e, &m, encode, None),
|
||||
None => e.zeros(w, h),
|
||||
}),
|
||||
});
|
||||
let matte3d = match matte3d {
|
||||
Some(m) => Some(m?),
|
||||
None => None,
|
||||
};
|
||||
composite_iso_with(e, r, ctx, split.layer(i), iso, &mut cv, 1.0, matte3d, None)?;
|
||||
}
|
||||
Some(cv)
|
||||
}
|
||||
|
||||
fn draw_collapsed<'a>(e: &mut Enc, r: &Renderer<'a>, ctx: &EvalCtx<'a>, layer: &Layer, item: ItemId, canvas: &mut GpuImage) -> Option<()> {
|
||||
let opacity = r.layer_opacity(ctx, layer);
|
||||
if opacity <= 0.0 {
|
||||
@@ -225,21 +283,44 @@ fn composite_layer(e: &mut Enc, r: &Renderer, ctx: &EvalCtx, layer: &Layer, buf:
|
||||
}
|
||||
|
||||
/// Track matte / Preserve Transparency on an isolated layer render, then blend.
|
||||
fn composite_iso(e: &mut Enc, r: &Renderer, ctx: &EvalCtx, layer: &Layer, mut iso: GpuImage, canvas: &mut GpuImage, opacity: f32) -> Option<()> {
|
||||
fn composite_iso(e: &mut Enc, r: &Renderer, ctx: &EvalCtx, layer: &Layer, iso: GpuImage, canvas: &mut GpuImage, opacity: f32) -> Option<()> {
|
||||
composite_iso_with(e, r, ctx, layer, iso, canvas, opacity, None, r.pipe().levels)
|
||||
}
|
||||
|
||||
/// `Renderer::composite_iso_with`: the matte from `matte_img` when given (a 3D matte drawn
|
||||
/// through the camera), else the matte layer placed in 2D; the blend quantised to `levels`.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn composite_iso_with(
|
||||
e: &mut Enc,
|
||||
r: &Renderer,
|
||||
ctx: &EvalCtx,
|
||||
layer: &Layer,
|
||||
mut iso: GpuImage,
|
||||
canvas: &mut GpuImage,
|
||||
opacity: f32,
|
||||
matte_img: Option<GpuImage>,
|
||||
levels: Option<f32>,
|
||||
) -> Option<()> {
|
||||
if let Some((m, kind)) = r.track_matte(ctx, layer) {
|
||||
let mut mimg = e.zeros(canvas.width, canvas.height);
|
||||
if m.is_active_at(ctx.time)
|
||||
&& let Some(mb) = r.layer_buf(ctx, m)
|
||||
{
|
||||
let mo = ctx.opacity(m) as f32;
|
||||
mimg = place(e, r, ctx, m, &mb, &mimg, BlendMode::Normal, mo, None)?;
|
||||
}
|
||||
let mimg = match matte_img.filter(|i| (i.width, i.height) == (canvas.width, canvas.height)) {
|
||||
Some(i) => i,
|
||||
None => {
|
||||
let mut mimg = e.zeros(canvas.width, canvas.height);
|
||||
if m.is_active_at(ctx.time)
|
||||
&& let Some(mb) = r.layer_buf(ctx, m)
|
||||
{
|
||||
let mo = ctx.opacity(m) as f32;
|
||||
mimg = place(e, r, ctx, m, &mb, &mimg, BlendMode::Normal, mo, None)?;
|
||||
}
|
||||
mimg
|
||||
}
|
||||
};
|
||||
iso = ops::matte(e, &iso, &mimg, kind);
|
||||
}
|
||||
if layer.preserve_transparency {
|
||||
iso = ops::preserve(e, &iso, canvas);
|
||||
}
|
||||
*canvas = ops::blend_full(e, canvas, &iso, layer.blend_mode, opacity, layer.id.0 as u32, r.pipe().levels);
|
||||
*canvas = ops::blend_full(e, canvas, &iso, layer.blend_mode, opacity, layer.id.0 as u32, levels);
|
||||
Some(())
|
||||
}
|
||||
|
||||
|
||||
@@ -1623,6 +1623,25 @@ impl<'a> Renderer<'a> {
|
||||
three_d::adv::prepare_run(self, ctx, run, out)
|
||||
}
|
||||
|
||||
/// A run of consecutive 3D layers of an Advanced 3D comp whose layers need the 2D
|
||||
/// compositing path (blend mode, track matte, Preserve Transparency), split for an
|
||||
/// accelerator's compositor ([`three_d::adv::SplitRun`]). `None` = [`Self::prepare_adv_run`]
|
||||
/// covers it, or it isn't an Advanced 3D run.
|
||||
pub fn split_adv_run<'p>(&'p self, ctx: &'p EvalCtx<'a>, run: &'p [&'p Layer], out: (u32, u32)) -> Option<three_d::adv::SplitRun<'p, 'a>> {
|
||||
three_d::adv::split_run(self, ctx, run, out)
|
||||
}
|
||||
|
||||
/// Whether the comp's 3D layers draw with the Advanced 3D renderer.
|
||||
pub fn is_adv3d(&self, ctx: &EvalCtx) -> bool {
|
||||
three_d::adv::active(self, ctx)
|
||||
}
|
||||
|
||||
/// An Environment Light Background layer ready to draw on an accelerator
|
||||
/// ([`three_d::SkyDraw`]; the CPU draws it in `draw_3d_run`). `None` = nothing to draw.
|
||||
pub fn sky_draw(&self, ctx: &EvalCtx, layer: &Layer) -> Option<three_d::SkyDraw> {
|
||||
three_d::compose::sky_draw(self, ctx, layer)
|
||||
}
|
||||
|
||||
/// The pixels a Wireframe-quality layer sets (white, opaque) on a `size` canvas: its
|
||||
/// bounds' outline, one pixel wide. Empty when the layer has no bounds.
|
||||
pub fn wireframe_pixels(&self, ctx: &EvalCtx, layer: &Layer, size: (u32, u32)) -> Vec<(u32, u32)> {
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
//! [`crate::Accelerator::raster_3d`], otherwise by the software rasteriser ([`raster`]), at 2×2
|
||||
//! supersampling. The resolve (box filter), depth of field (a gather blur by each pixel's
|
||||
//! circle of confusion, shaped by the camera's iris: [`dof`]) and the conversion back to the working encoding run on the CPU for
|
||||
//! both paths, and the result is composited over the layers below the run.
|
||||
//! both paths, and the result is composited over the layers below the run. An accelerator's
|
||||
//! compositor runs all of it on the device: [`Prepared`] for whole runs, [`SplitRun`] for runs
|
||||
//! with layers on the 2D compositing path (`draw_run`'s steps as data).
|
||||
|
||||
pub mod dof;
|
||||
pub mod raster;
|
||||
@@ -217,7 +219,7 @@ pub(crate) fn render_layers(r: &Renderer, ctx: &EvalCtx, layers: &[&Layer], out:
|
||||
|
||||
/// The run as one prepared scene for an accelerator's compositor when it can be composited
|
||||
/// directly over the canvas (Normal-mode layers without track mattes or Preserve Transparency;
|
||||
/// those go through [`draw_run`]'s 2D path; environment backgrounds draw their sky first). `None`
|
||||
/// those go through `draw_run`'s 2D path; environment backgrounds draw their sky first). `None`
|
||||
/// when the comp isn't Advanced 3D.
|
||||
pub(crate) fn prepare_run<'p, 'a>(r: &'p Renderer<'a>, ctx: &'p EvalCtx<'a>, run: &'p [&'p Layer], out: (u32, u32)) -> Option<Prepared<'p, 'a>> {
|
||||
if !active(r, ctx) || run.iter().any(|l| l.environment_background || needs_2d_composite(ctx, l)) {
|
||||
@@ -226,6 +228,102 @@ pub(crate) fn prepare_run<'p, 'a>(r: &'p Renderer<'a>, ctx: &'p EvalCtx<'a>, run
|
||||
Some(Prepared::new(r, ctx, run, out))
|
||||
}
|
||||
|
||||
/// A run of an Advanced 3D comp whose layers need the 2D compositing path (blend modes, track
|
||||
/// mattes, Preserve Transparency), prepared for an accelerator's compositor: `draw_run`'s
|
||||
/// steps as data. The plain layers render as one scene ([`SplitRun::main`]) composited over the
|
||||
/// canvas; then each special layer, from the farthest to the nearest, renders on its own
|
||||
/// ([`SplitRun::special`]), is hidden where the main scene is nearer, and goes through the 2D
|
||||
/// path with its track matte (a 3D matte layer drawn through the camera:
|
||||
/// [`SplitRun::with_matte3d`]).
|
||||
pub struct SplitRun<'p, 'a> {
|
||||
r: &'p Renderer<'a>,
|
||||
ctx: &'p EvalCtx<'a>,
|
||||
out: (u32, u32),
|
||||
plain: Vec<&'p Layer>,
|
||||
/// Far to near.
|
||||
special: Vec<&'p Layer>,
|
||||
/// Per special layer: its 3D track matte drawn solo (and whether it is active now).
|
||||
mattes: Vec<Option<(Layer, bool)>>,
|
||||
}
|
||||
|
||||
impl<'p, 'a> SplitRun<'p, 'a> {
|
||||
/// The plain layers as one scene.
|
||||
pub fn main(&self) -> Prepared<'_, 'a> {
|
||||
Prepared::new(self.r, self.ctx, &self.plain, self.out)
|
||||
}
|
||||
|
||||
/// Number of special layers.
|
||||
pub fn len(&self) -> usize {
|
||||
self.special.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.special.is_empty()
|
||||
}
|
||||
|
||||
/// Special layer `i` (far to near).
|
||||
pub fn layer(&self, i: usize) -> &'p Layer {
|
||||
self.special[i]
|
||||
}
|
||||
|
||||
/// Special layer `i` rendered on its own.
|
||||
pub fn special(&self, i: usize) -> Prepared<'_, 'a> {
|
||||
Prepared::new(self.r, self.ctx, std::slice::from_ref(&self.special[i]), self.out)
|
||||
}
|
||||
|
||||
/// `f` with special layer `i`'s 3D track matte as a prepared run (`None` when the matte
|
||||
/// layer isn't active: a transparent matte). `None` when the layer has no 3D matte (its
|
||||
/// matte, if any, is placed in 2D as usual).
|
||||
pub fn with_matte3d<R>(&self, i: usize, f: impl FnOnce(Option<&Prepared<'_, 'a>>) -> R) -> Option<R> {
|
||||
let (solo, active) = self.mattes[i].as_ref()?;
|
||||
if !*active {
|
||||
return Some(f(None));
|
||||
}
|
||||
let layers = [solo];
|
||||
let p = Prepared::new(self.r, self.ctx, &layers, self.out);
|
||||
Some(f(Some(&p)))
|
||||
}
|
||||
}
|
||||
|
||||
/// The run split for an accelerator when some layer needs the 2D compositing path (`None` when
|
||||
/// the comp isn't Advanced 3D, a layer is an environment background, or no layer needs it:
|
||||
/// `prepare_run` then covers the run).
|
||||
pub(crate) fn split_run<'p, 'a>(r: &'p Renderer<'a>, ctx: &'p EvalCtx<'a>, run: &'p [&'p Layer], out: (u32, u32)) -> Option<SplitRun<'p, 'a>> {
|
||||
if !active(r, ctx) || run.iter().any(|l| l.environment_background) || !run.iter().any(|l| needs_2d_composite(ctx, l)) {
|
||||
return None;
|
||||
}
|
||||
let (special, plain): (Vec<&Layer>, Vec<&Layer>) = run.iter().copied().partition(|l| needs_2d_composite(ctx, l));
|
||||
let special = far_to_near(r, ctx, special);
|
||||
let mattes = special.iter().map(|l| solo_matte(ctx, l)).collect();
|
||||
Some(SplitRun { r, ctx, out, plain, special, mattes })
|
||||
}
|
||||
|
||||
/// Special layers sorted by their anchor's camera depth, farthest first.
|
||||
fn far_to_near<'p>(r: &Renderer, ctx: &EvalCtx, special: Vec<&'p Layer>) -> Vec<&'p Layer> {
|
||||
let cam = camera_for(r, ctx);
|
||||
let mut order: Vec<(f64, &Layer)> = special
|
||||
.into_iter()
|
||||
.map(|l| {
|
||||
let anchor = l.transform().map_or([0.0; 3], |tr| ctx.v3(l, tr, "anchor", [0.0; 3]));
|
||||
(cam.depth(ctx.world_matrix(l).apply(effectcraft_geom::Vec3::from(anchor))), l)
|
||||
})
|
||||
.collect();
|
||||
order.sort_by(|a, b| b.0.total_cmp(&a.0));
|
||||
order.into_iter().map(|(_, l)| l).collect()
|
||||
}
|
||||
|
||||
/// A layer's 3D track matte, drawn through the camera on its own (no matte, Normal mode, no
|
||||
/// Preserve Transparency, visible), and whether it is active now.
|
||||
fn solo_matte(ctx: &EvalCtx, l: &Layer) -> Option<(Layer, bool)> {
|
||||
let m = l.track_matte.and_then(|tm| ctx.comp.layer(tm.layer).filter(|m| m.id != l.id && m.is_3d()))?;
|
||||
let mut solo = m.clone();
|
||||
solo.track_matte = None;
|
||||
solo.blend_mode = effectcraft_color::BlendMode::Normal;
|
||||
solo.preserve_transparency = false;
|
||||
solo.switches.video = true;
|
||||
Some((solo, m.is_active_at(ctx.time)))
|
||||
}
|
||||
|
||||
/// Layers whose blend mode, track matte or Preserve Transparency needs the 2D compositing path.
|
||||
fn needs_2d_composite(ctx: &EvalCtx, l: &Layer) -> bool {
|
||||
l.blend_mode != effectcraft_color::BlendMode::Normal
|
||||
@@ -252,16 +350,7 @@ pub(crate) fn draw_run(r: &Renderer, ctx: &EvalCtx, run: &[&Layer], canvas: &mut
|
||||
if special.is_empty() {
|
||||
return;
|
||||
}
|
||||
let cam = camera_for(r, ctx);
|
||||
let mut order: Vec<(f64, &Layer)> = special
|
||||
.into_iter()
|
||||
.map(|l| {
|
||||
let anchor = l.transform().map_or([0.0; 3], |tr| ctx.v3(l, tr, "anchor", [0.0; 3]));
|
||||
(cam.depth(ctx.world_matrix(l).apply(effectcraft_geom::Vec3::from(anchor))), l)
|
||||
})
|
||||
.collect();
|
||||
order.sort_by(|a, b| b.0.total_cmp(&a.0));
|
||||
for (_, l) in order {
|
||||
for l in far_to_near(r, ctx, special) {
|
||||
let Some((mut iso, depth)) = render_layers(r, ctx, &[l], out) else { continue };
|
||||
if let Some((m, md)) = &main {
|
||||
iso.data.par_iter_mut().zip(depth.par_iter()).zip(m.data.par_iter().zip(md.par_iter())).for_each(|((p, z), (q, mz))| {
|
||||
@@ -274,17 +363,8 @@ pub(crate) fn draw_run(r: &Renderer, ctx: &EvalCtx, run: &[&Layer], canvas: &mut
|
||||
});
|
||||
}
|
||||
// A 3D matte layer is drawn through the camera too.
|
||||
let matte3d = l.track_matte.and_then(|tm| ctx.comp.layer(tm.layer).filter(|m| m.id != l.id && m.is_3d())).map(|m| {
|
||||
let mut solo = m.clone();
|
||||
solo.track_matte = None;
|
||||
solo.blend_mode = effectcraft_color::BlendMode::Normal;
|
||||
solo.preserve_transparency = false;
|
||||
solo.switches.video = true;
|
||||
if m.is_active_at(ctx.time) {
|
||||
render_layers(r, ctx, &[&solo], out).map(|(i, _)| i).unwrap_or_else(|| Image::new(out.0, out.1))
|
||||
} else {
|
||||
Image::new(out.0, out.1)
|
||||
}
|
||||
let matte3d = solo_matte(ctx, l).map(|(solo, active)| {
|
||||
if active { render_layers(r, ctx, &[&solo], out).map(|(i, _)| i).unwrap_or_else(|| Image::new(out.0, out.1)) } else { Image::new(out.0, out.1) }
|
||||
});
|
||||
r.composite_iso_with(ctx, l, iso, canvas, 1.0, matte3d);
|
||||
}
|
||||
|
||||
@@ -971,30 +971,71 @@ pub(crate) fn environment_rotation(ctx: &EvalCtx) -> f32 {
|
||||
.to_radians() as f32
|
||||
}
|
||||
|
||||
/// Draw an Environment Light Background layer: its (equirectangular) image looked up by each
|
||||
/// pixel's view direction through the camera, as an infinitely distant backdrop.
|
||||
pub(crate) fn draw_sky(r: &Renderer, ctx: &EvalCtx, layer: &Layer, canvas: &mut Image) {
|
||||
let Some(buf) = r.blend_layer_buf(ctx, layer) else { return };
|
||||
let img = &buf.img;
|
||||
if img.width == 0 || img.height == 0 {
|
||||
return;
|
||||
/// An Environment Light Background layer ready to draw (see [`draw_sky`]): the layer's
|
||||
/// buffer in the blending space, its opacity and the camera's view rays. Also handed to an
|
||||
/// accelerator's compositor (`Renderer::sky_draw`).
|
||||
#[derive(Clone)]
|
||||
pub struct SkyDraw {
|
||||
pub buf: Arc<Buf>,
|
||||
pub opacity: f32,
|
||||
/// Camera basis: forward, right, down.
|
||||
pub fwd: [f64; 3],
|
||||
pub right: [f64; 3],
|
||||
pub down: [f64; 3],
|
||||
pub zoom: f64,
|
||||
pub ortho: bool,
|
||||
/// View size (comp pixels), region-of-interest offset (output pixels), output scale.
|
||||
pub view: (f64, f64),
|
||||
pub roi: (f64, f64),
|
||||
pub scale: f64,
|
||||
/// The Environment light's rotation (radians).
|
||||
pub rotation: f32,
|
||||
}
|
||||
|
||||
pub(crate) fn sky_draw(r: &Renderer, ctx: &EvalCtx, layer: &Layer) -> Option<SkyDraw> {
|
||||
let buf = r.blend_layer_buf(ctx, layer)?;
|
||||
if buf.img.width == 0 || buf.img.height == 0 {
|
||||
return None;
|
||||
}
|
||||
let op = (ctx.opacity(layer) as f32 * r.opacity_mul()).clamp(0.0, 1.0);
|
||||
if op <= 0.0 {
|
||||
return;
|
||||
return None;
|
||||
}
|
||||
let cam = camera_for(r, ctx);
|
||||
let (fwd, right, down) = (cam.forward(), cam.right(), cam.down());
|
||||
let s = r.opts.scale.max(1e-9);
|
||||
let (cw, ch) = view_size(r, ctx);
|
||||
let (ox, oy) = r.roi_offset().map_or((0.0, 0.0), |(x, y, _, _)| (x, y));
|
||||
let rot = environment_rotation(ctx);
|
||||
let v3 = |v: Vec3| [v.x, v.y, v.z];
|
||||
Some(SkyDraw {
|
||||
buf,
|
||||
opacity: op,
|
||||
fwd: v3(cam.forward()),
|
||||
right: v3(cam.right()),
|
||||
down: v3(cam.down()),
|
||||
zoom: cam.zoom,
|
||||
ortho: cam.ortho,
|
||||
view: view_size(r, ctx),
|
||||
roi: r.roi_offset().map_or((0.0, 0.0), |(x, y, _, _)| (x, y)),
|
||||
scale: r.opts.scale.max(1e-9),
|
||||
rotation: environment_rotation(ctx),
|
||||
})
|
||||
}
|
||||
|
||||
/// Draw an Environment Light Background layer: its (equirectangular) image looked up by each
|
||||
/// pixel's view direction through the camera, as an infinitely distant backdrop.
|
||||
pub(crate) fn draw_sky(r: &Renderer, ctx: &EvalCtx, layer: &Layer, canvas: &mut Image) {
|
||||
let Some(sky) = sky_draw(r, ctx, layer) else { return };
|
||||
let img = &sky.buf.img;
|
||||
let op = sky.opacity;
|
||||
let v3 = |v: [f64; 3]| vec3(v[0], v[1], v[2]);
|
||||
let (fwd, right, down) = (v3(sky.fwd), v3(sky.right), v3(sky.down));
|
||||
let s = sky.scale;
|
||||
let (cw, ch) = sky.view;
|
||||
let (ox, oy) = sky.roi;
|
||||
let rot = sky.rotation;
|
||||
let (iw, ih) = (img.width as f64, img.height as f64);
|
||||
canvas.rows_mut().for_each(|(y, row)| {
|
||||
let cy = (y as f64 + 0.5 + oy) / s;
|
||||
for (x, d) in row.iter_mut().enumerate() {
|
||||
let cx = (x as f64 + 0.5 + ox) / s;
|
||||
let dir = if cam.ortho { fwd } else { (fwd * cam.zoom + right * (cx - cw / 2.0) + down * (cy - ch / 2.0)).normalize() };
|
||||
let dir = if sky.ortho { fwd } else { (fwd * sky.zoom + right * (cx - cw / 2.0) + down * (cy - ch / 2.0)).normalize() };
|
||||
let (u, v) = super::adv::shade::equirect_uv([dir.x as f32, dir.y as f32, dir.z as f32], rot);
|
||||
// Wrap horizontally across the seam.
|
||||
let sx = (u as f64 * iw).clamp(0.5, iw - 0.5);
|
||||
|
||||
@@ -17,7 +17,7 @@ mod tests;
|
||||
mod tests_orient;
|
||||
|
||||
pub use camera::{CameraState, Dof, Rig, View3D, ViewCam, Views3D, active_camera, default_camera, default_view_cam};
|
||||
pub use compose::{Geo as PlaneGeo, Plane3d, PlaneDof, Run3d};
|
||||
pub use compose::{Geo as PlaneGeo, Plane3d, PlaneDof, Run3d, SkyDraw};
|
||||
pub use light::{LightState, Material, lights_at};
|
||||
|
||||
use effectcraft_geom::Mat3;
|
||||
|
||||
+24
-10
@@ -199,21 +199,35 @@ runs render on the GPU end to end (`gpu::adv3d`, `Renderer::prepare_adv_run` /
|
||||
then compute kernels (`adv3d.wgsl`) resolve the 2×2 supersampling, average the sub-samples
|
||||
(nearest depth), apply the depth-based iris depth of field with the Classic 3D bokeh spans
|
||||
(`three_d::bokeh::kernel_spans`, highlight boost, progressive blur levels) and composite over the
|
||||
GPU canvas; only the depth of field reads back its 8-byte radius range. Advanced 3D layers with
|
||||
blend modes, track mattes or Preserve Transparency and environment backgrounds take the CPU's
|
||||
2D compositing path (their scenes still render through `render_3d`). Wireframe outlines draw on
|
||||
the GPU from the CPU's pixel list (`Renderer::wireframe_pixels`). GPU
|
||||
effects (`effects::GPU_EFFECTS`, 166 of them: blurs, colour correction, keying incl. Key Light,
|
||||
mattes, channel, stylize, distortion and warps (Warp, Bezier Warp, Smear, Reshape, CC Bend It, CC
|
||||
Page Turn), Cartoon, bevels, shapes, transitions, generators, noise, grain and time; see
|
||||
GPU canvas; only the depth of field reads back its 8-byte radius range. Runs whose layers need
|
||||
the 2D compositing path (blend modes, track mattes, Preserve Transparency) come to the GPU as a
|
||||
`three_d::adv::SplitRun` (`Renderer::split_adv_run`, `draw_run`'s steps as data): the plain
|
||||
layers render as one scene over the canvas, then each special layer, farthest first, renders on
|
||||
its own, is hidden where the main scene is nearer (`occlude`), and goes through the 2D matte /
|
||||
Preserve Transparency / blend kernels (a 3D track matte renders solo through the camera).
|
||||
Environment Light Background layers draw their sky in a kernel (`adv_sky`, from
|
||||
`Renderer::sky_draw`) before the rest of the run, in Classic and Advanced 3D comps. Wireframe
|
||||
outlines draw on the GPU from the CPU's pixel list (`Renderer::wireframe_pixels`). GPU effects
|
||||
(`effects::GPU_EFFECTS`, 196 of them: blurs, colour correction, keying incl. Key Light, mattes,
|
||||
channel, stylize, distortion and warps (Warp, Bezier Warp, Smear, Reshape, CC Bend It, CC Page
|
||||
Turn, CC Bender, CC Blobbylize), Cartoon, bevels, shapes, the transitions (CC light family, CC
|
||||
transitions, Block Dissolve), perspective (Radial Shadow, CC Cylinder / Sphere / Spotlight /
|
||||
Environment, 3D Glasses), generators, noise, grain, text (Numbers, Timecode) and time; see
|
||||
[effects.md](effects.md)) repeat the CPU effect's steps (padding, box radii, parameters, hashes)
|
||||
as kernels, in one module per family (`gpu::fx_*` with `shaders/fx_*.wgsl`); consecutive GPU
|
||||
effects run as one chain with one upload and one readback. Statistics that need the whole frame
|
||||
(Auto Levels / Contrast / Color, Equalize, Shadow/Highlight, Color Stabilizer, Remove Grain's
|
||||
noise level) are measured on the CPU from one readback and applied on the GPU; effects reading
|
||||
other frames (Echo, Posterize Time) upload the frames the host renders. Settings a kernel cannot
|
||||
match render on the CPU (`effects::catalog::gpu_supported`; e.g. Warp's Fisheye and Twist past
|
||||
50 % or with a distortion, whose Newton inverse wanders chaotically near the crease). Tests render
|
||||
other frames (Echo, Posterize Time, Time Difference, Time Displacement, CC Force Motion Blur, CC
|
||||
Wide Time, Pixel Motion Blur, Timewarp) upload the frames the host renders (`time_frames`) and
|
||||
combine them on the GPU; Time Displacement's per-pixel times and Timewarp's motion vectors
|
||||
(`time_fx::timewarp_plan`) are computed on the CPU. Numbers and Timecode rasterise their glyph
|
||||
coverage on the CPU (`textfx::text_layer`) and composite it on the GPU. Other layers an effect
|
||||
reads (gradients, reveals, environments) are fitted on the CPU (`util::fit_layer`) and uploaded.
|
||||
Settings a kernel cannot match render on the CPU (`effects::catalog::gpu_supported`; e.g. Warp's
|
||||
Fisheye and Twist past 50 % or with a distortion, whose Newton inverse wanders chaotically near
|
||||
the crease, and Timewarp with a Matte Layer); Card Wipe (the 3D card renderer of Card Dance and
|
||||
Shatter) stays on the CPU. Tests render
|
||||
scenes on both paths and compare them (≤ 1/255 at 8 bpc, ≤ 1e-3 at 32 bpc); they skip without an
|
||||
adapter. Working textures (premultiplied RGBA f32) come from a pool: a released texture is reused
|
||||
once every encoder that could still read it has been submitted, transparent inputs share one
|
||||
|
||||
+31
-31
@@ -5,7 +5,7 @@
|
||||
|
||||
EffectCraft ships 306 effects with 3009 parameters in total, grouped into the same categories as the Effects & Presets panel. Every effect is our own implementation, written from public behaviour descriptions and standard image-processing literature. Parameter names, order, popup options, units, defaults and ranges follow the reference application so that projects, expressions and muscle memory carry over.
|
||||
|
||||
- **GPU**: 166 effects also run on the GPU compositor with identical results.
|
||||
- **GPU**: 196 effects also run on the GPU compositor with identical results.
|
||||
- **32**: 306 effects process 32-bit float (HDR, overbright) pixels without clamping.
|
||||
- **Status**: 306 implemented in full, 0 partial (what is missing is listed).
|
||||
|
||||
@@ -133,8 +133,8 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
||||
| Bezier Warp | `ec.distort.bezierwarp` | 13 | GPU | 32 | Implemented |
|
||||
| Bulge | `ec.distort.bulge` | 6 | GPU | 32 | Implemented |
|
||||
| CC Bend It | `ec.distort.ccbendit` | 4 | GPU | 32 | Implemented |
|
||||
| CC Bender | `ec.distort.ccbender` | 5 | | 32 | Implemented |
|
||||
| CC Blobbylize | `ec.distort.ccblobbylize` | 15 | | 32 | Implemented |
|
||||
| CC Bender | `ec.distort.ccbender` | 5 | GPU | 32 | Implemented |
|
||||
| CC Blobbylize | `ec.distort.ccblobbylize` | 15 | GPU | 32 | Implemented |
|
||||
| CC Flo Motion | `ec.distort.ccflomotion` | 7 | GPU | 32 | Implemented |
|
||||
| CC Griddler | `ec.distort.ccgriddler` | 5 | GPU | 32 | Implemented |
|
||||
| CC Lens | `ec.distort.cclens` | 3 | GPU | 32 | Implemented |
|
||||
@@ -194,9 +194,9 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
||||
| Audio Waveform | `ec.generate.audiowaveform` | 17 | | 32 | Implemented |
|
||||
| Beam | `ec.generate.beam` | 11 | | 32 | Implemented |
|
||||
| CC Glue Gun | `ec.generate.ccgluegun` | 7 | | 32 | Implemented |
|
||||
| CC Light Burst 2.5 | `ec.generate.cclightburst` | 6 | | 32 | Implemented |
|
||||
| CC Light Rays | `ec.generate.cclightrays` | 8 | | 32 | Implemented |
|
||||
| CC Light Sweep | `ec.generate.cclightsweep` | 9 | | 32 | Implemented |
|
||||
| CC Light Burst 2.5 | `ec.generate.cclightburst` | 6 | GPU | 32 | Implemented |
|
||||
| CC Light Rays | `ec.generate.cclightrays` | 8 | GPU | 32 | Implemented |
|
||||
| CC Light Sweep | `ec.generate.cclightsweep` | 9 | GPU | 32 | Implemented |
|
||||
| CC Threads | `ec.generate.ccthreads` | 8 | | 32 | Implemented |
|
||||
| Cell Pattern | `ec.generate.cellpattern` | 14 | GPU | 32 | Implemented |
|
||||
| Checkerboard | `ec.generate.checkerboard` | 10 | GPU | 32 | Implemented |
|
||||
@@ -303,15 +303,15 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
||||
| Effect | Id | Params | GPU | 32 | Status |
|
||||
|---|---|---:|:-:|:-:|---|
|
||||
| 3D Camera Tracker | `ec.perspective.cameratracker` | 12 | | 32 | Implemented |
|
||||
| 3D Glasses | `ec.perspective.3dglasses` | 8 | | 32 | Implemented |
|
||||
| 3D Glasses | `ec.perspective.3dglasses` | 8 | GPU | 32 | Implemented |
|
||||
| Bevel Alpha | `ec.perspective.bevelalpha` | 4 | GPU | 32 | Implemented |
|
||||
| Bevel Edges | `ec.perspective.beveledges` | 4 | GPU | 32 | Implemented |
|
||||
| CC Cylinder | `ec.perspective.cccylinder` | 12 | | 32 | Implemented |
|
||||
| CC Environment | `ec.perspective.ccenvironment` | 4 | | 32 | Implemented |
|
||||
| CC Sphere | `ec.perspective.ccsphere` | 14 | | 32 | Implemented |
|
||||
| CC Spotlight | `ec.perspective.ccspotlight` | 8 | | 32 | Implemented |
|
||||
| CC Cylinder | `ec.perspective.cccylinder` | 12 | GPU | 32 | Implemented |
|
||||
| CC Environment | `ec.perspective.ccenvironment` | 4 | GPU | 32 | Implemented |
|
||||
| CC Sphere | `ec.perspective.ccsphere` | 14 | GPU | 32 | Implemented |
|
||||
| CC Spotlight | `ec.perspective.ccspotlight` | 8 | GPU | 32 | Implemented |
|
||||
| Drop Shadow | `ec.perspective.dropshadow` | 6 | GPU | 32 | Implemented |
|
||||
| Radial Shadow | `ec.perspective.radialshadow` | 9 | | 32 | Implemented |
|
||||
| Radial Shadow | `ec.perspective.radialshadow` | 9 | GPU | 32 | Implemented |
|
||||
|
||||
## Simulation
|
||||
|
||||
@@ -370,37 +370,37 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
||||
|
||||
| Effect | Id | Params | GPU | 32 | Status |
|
||||
|---|---|---:|:-:|:-:|---|
|
||||
| Numbers | `ec.text.numbers` | 14 | | 32 | Implemented |
|
||||
| Timecode | `ec.text.timecode` | 12 | | 32 | Implemented |
|
||||
| Numbers | `ec.text.numbers` | 14 | GPU | 32 | Implemented |
|
||||
| Timecode | `ec.text.timecode` | 12 | GPU | 32 | Implemented |
|
||||
|
||||
## Time
|
||||
|
||||
| Effect | Id | Params | GPU | 32 | Status |
|
||||
|---|---|---:|:-:|:-:|---|
|
||||
| CC Force Motion Blur | `ec.time.ccforcemotionblur` | 4 | | 32 | Implemented |
|
||||
| CC Wide Time | `ec.time.ccwidetime` | 3 | | 32 | Implemented |
|
||||
| CC Force Motion Blur | `ec.time.ccforcemotionblur` | 4 | GPU | 32 | Implemented |
|
||||
| CC Wide Time | `ec.time.ccwidetime` | 3 | GPU | 32 | Implemented |
|
||||
| Echo | `ec.time.echo` | 5 | GPU | 32 | Implemented |
|
||||
| Pixel Motion Blur | `ec.time.pixelmotionblur` | 4 | | 32 | Implemented |
|
||||
| Pixel Motion Blur | `ec.time.pixelmotionblur` | 4 | GPU | 32 | Implemented |
|
||||
| Posterize Time | `ec.time.posterizetime` | 1 | GPU | 32 | Implemented |
|
||||
| Time Difference | `ec.time.timedifference` | 5 | | 32 | Implemented |
|
||||
| Time Displacement | `ec.time.timedisplacement` | 4 | | 32 | Implemented |
|
||||
| Timewarp | `ec.time.timewarp` | 28 | | 32 | Implemented |
|
||||
| Time Difference | `ec.time.timedifference` | 5 | GPU | 32 | Implemented |
|
||||
| Time Displacement | `ec.time.timedisplacement` | 4 | GPU | 32 | Implemented |
|
||||
| Timewarp | `ec.time.timewarp` | 28 | GPU | 32 | Implemented |
|
||||
|
||||
## Transition
|
||||
|
||||
| Effect | Id | Params | GPU | 32 | Status |
|
||||
|---|---|---:|:-:|:-:|---|
|
||||
| Block Dissolve | `ec.transition.blockdissolve` | 5 | | 32 | Implemented |
|
||||
| CC Glass Wipe | `ec.transition.ccglasswipe` | 5 | | 32 | Implemented |
|
||||
| CC Grid Wipe | `ec.transition.ccgridwipe` | 7 | | 32 | Implemented |
|
||||
| CC Image Wipe | `ec.transition.ccimagewipe` | 7 | | 32 | Implemented |
|
||||
| CC Jaws | `ec.transition.ccjaws` | 6 | | 32 | Implemented |
|
||||
| CC Light Wipe | `ec.transition.cclightwipe` | 8 | | 32 | Implemented |
|
||||
| CC Line Sweep | `ec.transition.cclinesweep` | 5 | | 32 | Implemented |
|
||||
| CC Radial ScaleWipe | `ec.transition.ccradialscalewipe` | 3 | | 32 | Implemented |
|
||||
| CC Scale Wipe | `ec.transition.ccscalewipe` | 3 | | 32 | Implemented |
|
||||
| CC Twister | `ec.transition.cctwister` | 5 | | 32 | Implemented |
|
||||
| CC WarpoMatic | `ec.transition.ccwarpomatic` | 8 | | 32 | Implemented |
|
||||
| Block Dissolve | `ec.transition.blockdissolve` | 5 | GPU | 32 | Implemented |
|
||||
| CC Glass Wipe | `ec.transition.ccglasswipe` | 5 | GPU | 32 | Implemented |
|
||||
| CC Grid Wipe | `ec.transition.ccgridwipe` | 7 | GPU | 32 | Implemented |
|
||||
| CC Image Wipe | `ec.transition.ccimagewipe` | 7 | GPU | 32 | Implemented |
|
||||
| CC Jaws | `ec.transition.ccjaws` | 6 | GPU | 32 | Implemented |
|
||||
| CC Light Wipe | `ec.transition.cclightwipe` | 8 | GPU | 32 | Implemented |
|
||||
| CC Line Sweep | `ec.transition.cclinesweep` | 5 | GPU | 32 | Implemented |
|
||||
| CC Radial ScaleWipe | `ec.transition.ccradialscalewipe` | 3 | GPU | 32 | Implemented |
|
||||
| CC Scale Wipe | `ec.transition.ccscalewipe` | 3 | GPU | 32 | Implemented |
|
||||
| CC Twister | `ec.transition.cctwister` | 5 | GPU | 32 | Implemented |
|
||||
| CC WarpoMatic | `ec.transition.ccwarpomatic` | 8 | GPU | 32 | Implemented |
|
||||
| Card Wipe | `ec.transition.cardwipe` | 44 | | 32 | Implemented |
|
||||
| Gradient Wipe | `ec.transition.gradientwipe` | 5 | GPU | 32 | Implemented |
|
||||
| Iris Wipe | `ec.transition.iriswipe` | 7 | GPU | 32 | Implemented |
|
||||
|
||||
+40
-3
@@ -10,7 +10,7 @@ How close EffectCraft is to After Effects 2026, feature by feature, and how much
|
||||
| Unweighted | ≈ 97% (half-credit) / 99% (fractions) |
|
||||
| P0 / P1 / P2 | ≈ 99% / 96% / 90% (half-credit); 99.8% / 99.2% / 97.5% (fractions) |
|
||||
| Features done / partial / missing | 87 / 5 / 0 of 92 |
|
||||
| **Effects** | **306** effects (all After Effects 2026 effects), every one implemented in full ([effects.md](effects.md)); 166 run on the GPU |
|
||||
| **Effects** | **306** effects (all After Effects 2026 effects), every one implemented in full ([effects.md](effects.md)); 196 run on the GPU |
|
||||
| Disabled menu entries left | 1, on purpose: Import ▸ Vanishing Point (.vpe), whose format has no public specification |
|
||||
| Remaining work | ≈ 9–11 agent-hours at the pace measured so far (≈ 29 on the conservative audit scale); ≈ 6–7 without the learned-model items |
|
||||
| **Wall-clock estimate** | **≈ 2–2.5 hours** with five agents in parallel; ≈ 1.5 hours for 100% of P0 + P1 |
|
||||
@@ -156,11 +156,11 @@ the disk cache in the formats wave; the 3D Camera Tracker in M12.6.)
|
||||
| Text | ≈ 96% | 0.3 | no extruded strokes (M13.12: the Variable Font Axes animator re-spaces the text — advances follow the animated axes; M13.6: variable font axes in the character style — `layer.setText variations`, the Character panel's Variable Font Axes fields — shape with HVAR / gvar advances and draw at that design-space position; OpenType features — stylistic sets, discretionary ligatures, contextual / stylistic alternates, swash, titling, ordinals, fractions, figure styles, true small caps / all small caps and superior / inferior glyphs with faux fallback — per character with the Character panel's OpenType popup and `text.fontFeatures` landed in M13.2; vertical Roman / Tate-Chu-Yoko, forced LTR paragraphs, caret on animated and path text, Variable Font Axes and Lottie style runs landed in M13.5; extruded, bevelled text in M7.6; per-character styles, paragraph settings, on-canvas editing and the `sourceText` style API in M9.9–M9.10) |
|
||||
| Web | 97% | 0.2 | GPU effects in Render Queue / analysis workers (job workers render on their CPU), layer buffers in the browser's disk cache (frames only), no shared-memory threads inside one engine instance (the decided design is one engine instance per worker; a threaded build needs nightly `build-std`) (browser storage, Web Audio, Web Worker renders/analyses, WebGPU viewer and offline install landed in M15.2; viewer frames in frame workers fed by project diffs, Roto Brush propagation in a worker, non-blocking `wait: true` jobs and a browser Media Browser (File System Access folders, browser storage) in M13.10; WEB-1 in M13.24: GPU effects and the GPU compositor in the frame workers on their own WebGPU devices with deferred readbacks (frames render in passes; Backend Auto per comp), the disk cache in the Origin Private File System (written by the workers with sync access handles, LRU under the settings' limit, served after a reload), the storage manager (Settings ▸ Disk ▸ Browser Storage, `storage.info` / `storage.persist` / `storage.clear`), and a WGSL constant Chrome rejected (it disabled every GPU kernel in Chrome) fixed) |
|
||||
| 3D | 88% | 5.5 | multi-view layouts, the Extended Viewer for Advanced 3D comps (Classic 3D Extended Viewer landed in M13.5 UI completion); collapsed precomps of another size seen through the parent's camera render (fixed in M13.2); stereo rigs, orbit nulls, lights controlled by the camera, cameras/lights from glTF models, environment backgrounds, Advanced 3D motion blur, blend modes and track mattes landed in M7.7; Classic 3D iris-shaped bokeh with highlights, progressive depth of field on tilted layers and the focus-link commands landed in M13.5; Advanced 3D depth of field with the iris and highlight options, collapsed precomps as real Advanced 3D geometry and extruded text/shape strokes landed in M13.8; Advanced 3D (glTF/OBJ models, primitives, extruded text and shapes, PBR, image-based light, shadow maps, GPU rasteriser) in M7.4–M7.6; Advanced 3D end to end on the GPU (motion blur, iris depth of field, compositing) in M13.11 |
|
||||
| Effects | ≈ 85% | 3.0 | GPU versions of the remaining effects (166 run on the GPU since M13.13, EFF-5: Warp, Bezier Warp, Reshape, Smear, CC Bend It, CC Page Turn, Cartoon, Color Emboss, Circle, Ellipse, Iris Wipe, Bevel Alpha / Edges and Gaussian Blur (Legacy) joined the 152 of M13.9; simulations, 3D channel, VR, audio-driven, OCIO, text and the CC light / transition families still render on the CPU) and the missing controls listed as partial in [effects.md](effects.md) (every After Effects effect exists since M9.11, M12.5 and M12.6; parameter names, order, twirl-downs, popups, units and defaults were aligned in M9.12) |
|
||||
| Effects | ≈ 85% | 3.0 | GPU versions of the remaining effects (196 run on the GPU since M13.22, EFF-5: the CC light family, the CC transitions, Block Dissolve, Radial Shadow, CC Bender / Blobbylize / Cylinder / Sphere / Spotlight / Environment, 3D Glasses, Numbers, Timecode and the time effects joined the 166 of M13.13; simulations, 3D channel, VR, audio-driven, OCIO and Card Wipe still render on the CPU) and the missing controls listed as partial in [effects.md](effects.md) (every After Effects effect exists since M9.11, M12.5 and M12.6; parameter names, order, twirl-downs, popups, units and defaults were aligned in M9.12) |
|
||||
| Interface | 75% | 5.0 | more Learn tutorials and pixel-level fidelity of dialogs (the Home ▸ Templates gallery (eight original built-in templates, user templates from File ▸ Save as Template…) and View ▸ Simulate Output ▸ My Custom RGB… landed in M13.25; Timeline layer reordering by drag, a non-snapping viewer pan, a working rename field and twirl arrows, and the full set of property reveal shortcuts — double presses, Alt+Shift keyframes, Ctrl+` — landed in M13.17–M13.20; the Home ▸ Learn tab with interactive tutorials and a UI fidelity pass landed in M13.5 UI completion; visual editors for Lumetri RGB / hue-saturation curves, Colorama's output cycle wheel, Glow's colour map and Reshape's correspondence points (viewer handles), a scrolling Preview panel and AE-style Composition / Timeline tabs (close, label swatch, viewer lock) landed in M13.10; Timeline outline and Project panel columns scroll horizontally, the Layer Style dialog, ROI resize handles, Pan Behind snapping and 3D Reference Axes landed in M13.5; native macOS menu bar, Timeline columns/search/reveal-add, Home screen with recent projects and all AE workspaces landed; viewer rulers/snapping/channels/snapshots landed in M0.13) |
|
||||
| Project | ≈ 68% | 5.5 | auto-save, folder moves, OCIO displays beyond the built-in tone map (Color Engine with OCIO/ACES working spaces, HDR compand/tone mapping, Rec. 2100 PQ/HLG output, Feet + Frames, display colour management, Simulate Output and the locked viewer landed in M7.7; proxies and Interpret Footage fields / pixel aspect / alpha guess landed: PRJ-8, PRJ-3) |
|
||||
| Masks & roto | 74% | 5.0 | Roto Brush's learned (3.0) segmentation model (variable-width mask feather points with the Mask Feather tool landed in M13.5; mask tracking and Mask Interpolation landed in M6.6; Roto Brush & Refine Edge with graph-cut segmentation, flow propagation, edge matting, decontamination and Freeze in M6.7) |
|
||||
| Preview | 77% | 2.8 | GPU compositing of Advanced 3D layers with blend modes / track mattes and of environment backgrounds (pooled GPU textures and fused quantisation since M13.13: Lower Third GPU warm 21 → 4 ms/frame, and Auto now picks the CPU or the GPU per comp from measured frame times; Advanced 3D runs — raster, motion blur, iris depth of field, compositing — and wireframes run on the GPU since M13.11: CPU warm 3394 → GPU warm 290 ms/frame at 1920×1080 (11.7×; Half 968 → 85 ms), GPU ≠ CPU on 0.005 % of pixels, on an M4 Pro under load; Classic 3D bokeh depth of field — iris shapes, highlights, fringe, progressive blur on tilted planes — runs in WGSL since M13.6: 3D Showcase GPU warm 205 → 54 ms/frame at full size on an M4 Pro; the Preview panel's five shortcuts with their own Include / Loop / Cache Before Playback / Range / Play From / Frame Rate / Skip / Resolution / Full Screen / stop options and `playback.settings.get/set` landed in M13.8; Classic 3D runs and adjustment layers composite on the GPU since M12.7; persistent disk cache with the blue cache bar landed; region of interest, snapshots, exposure and Fast Previews landed in M0.13) |
|
||||
| Preview | 77% | 2.8 | GPU effects for simulations, 3D channel, VR, OCIO and Card Wipe (Advanced 3D layers with blend modes / track mattes / Preserve Transparency and environment backgrounds composite on the GPU since M13.22; pooled GPU textures and fused quantisation since M13.13: Lower Third GPU warm 21 → 4 ms/frame, and Auto now picks the CPU or the GPU per comp from measured frame times; Advanced 3D runs — raster, motion blur, iris depth of field, compositing — and wireframes run on the GPU since M13.11: CPU warm 3394 → GPU warm 290 ms/frame at 1920×1080 (11.7×; Half 968 → 85 ms), GPU ≠ CPU on 0.005 % of pixels, on an M4 Pro under load; Classic 3D bokeh depth of field — iris shapes, highlights, fringe, progressive blur on tilted planes — runs in WGSL since M13.6: 3D Showcase GPU warm 205 → 54 ms/frame at full size on an M4 Pro; the Preview panel's five shortcuts with their own Include / Loop / Cache Before Playback / Range / Play From / Frame Rate / Skip / Resolution / Full Screen / stop options and `playback.settings.get/set` landed in M13.8; Classic 3D runs and adjustment layers composite on the GPU since M12.7; persistent disk cache with the blue cache bar landed; region of interest, snapshots, exposure and Fast Previews landed in M0.13) |
|
||||
| Tracking | ≈ 96% | 0.3 | face tracking is a classical (skin model + feature components + shape model) fitter, not a learned detector: profile views and occluded faces are weak; Rolling Shutter Ripple is approximated by Subspace Warp's mesh density (face tracking (Outline Only / Detailed Features with Face Track Points and Extract & Copy Face Measurements), Subspace Warp's content-preserving mesh warp on subspace-smoothed trajectories, and radial lens distortion (k1, k2) in the camera bundle adjustment with Undistort Footage landed in M13.3; Rolling Shutter Repair in M9.11; point tracker, mask tracking, Warp Stabilizer and the 3D Camera Tracker in M6.x / M12.5 / M12.6) |
|
||||
| Paint | 0% | 4.0 | Brush, Clone Stamp, Eraser |
|
||||
|
||||
@@ -224,6 +224,43 @@ axes since M13.6; Advanced 3D's DOF has iris shapes since M13.8).
|
||||
as real geometry lit by the parent (2D collapsed precomps draw nested 3D layers with the
|
||||
parent's renderer); text and shape strokes extrude as bevelled meshes in paint order.
|
||||
|
||||
## Update: M13.22 GPU effects, part A
|
||||
|
||||
- **GPU ports (EFF-5)**: 30 more effects run on the GPU (196 GPU effects), each matching the
|
||||
CPU oracle within 1/255 (8 bpc) / 1e-3 (32 bpc) on every tested pixel, directly on a buffer
|
||||
(full and half resolution, as adjustment) and composited at 8 and 32 bpc:
|
||||
- the CC light family: CC Light Rays, CC Light Burst 2.5, CC Light Sweep, CC Light Wipe
|
||||
(`gpu::fx_light`);
|
||||
- transitions and perspective (`gpu::fx_transition`): Block Dissolve (block indices computed
|
||||
on the CPU per column / row sub-sample, so the hash sees the CPU's integers), CC Glass Wipe,
|
||||
CC Grid Wipe, CC Image Wipe, CC Jaws, CC Line Sweep, CC Radial ScaleWipe, CC Scale Wipe, CC
|
||||
Twister, CC WarpoMatic, Radial Shadow, CC Bender, CC Blobbylize, CC Cylinder, CC Sphere, CC
|
||||
Spotlight, CC Environment, 3D Glasses; other layers they read (gradient, reveal, backside,
|
||||
environment, stereo views) are fitted on the CPU and uploaded;
|
||||
- Numbers and Timecode (`gpu::fx_text`): the glyph coverage (fill and stroke ring) is
|
||||
rasterised on the CPU with the effect's own stroke font and composited on the GPU with the
|
||||
box, fill and stroke;
|
||||
- time (`gpu::fx_time`): Time Difference, Time Displacement, CC Force Motion Blur, CC Wide
|
||||
Time, Pixel Motion Blur and Timewarp fetch their frames through the host as Echo does and
|
||||
combine them on the GPU (weighted sums one frame per pass, in the CPU's order); Time
|
||||
Displacement's per-pixel times and Timewarp's motion vectors (block matching and smoothing)
|
||||
are computed on the CPU, Timewarp's Whole Frames / Frame Mix / Pixel Motion frame building
|
||||
and shutter average on the GPU.
|
||||
- **Fallbacks and deviations**: Timewarp with a Matte Layer renders on the CPU
|
||||
(`gpu_supported`); Card Wipe (the 3D card renderer shared with Card Dance and Shatter) and the
|
||||
3D Camera Tracker stay on the CPU. CC Environment now fades nearly transparent environment
|
||||
texels to black continuously (colour / max(alpha, 1e-3), on both paths) instead of a hard
|
||||
1e-6 un-premultiply cut-off, which turned the filtered map's float noise into colour. CC
|
||||
Sphere / CC Environment keep their longitude on the side of the ±180° seam the ray's sign
|
||||
puts it (a GPU's approximate `atan2` can land across it).
|
||||
- **Advanced 3D compositing on the GPU (3D-3, PRV-3)**: runs with blend modes, track mattes
|
||||
(2D, or 3D mattes drawn through the camera) or Preserve Transparency no longer read back:
|
||||
`Renderer::split_adv_run` hands `draw_run`'s split path to the GPU (main scene, then each
|
||||
special layer far to near, hidden behind the nearer main scene and composited through the 2D
|
||||
kernels), and Environment Light Background skies draw in a kernel in Classic and Advanced 3D
|
||||
comps. The GPU walk matches the CPU compositor on the same rasters exactly at 8 and 32 bpc,
|
||||
with only the frame's own readback.
|
||||
|
||||
## Update: M13.13 GPU performance and the remaining GPU ports
|
||||
|
||||
- **Small comps on the GPU (PRV-3)**: the GPU frame allocated (and zero-initialised) several
|
||||
|
||||
Reference in new issue
Block a user