Merge branch 'main' into worktree-agent-a139f16b3a4130998

This commit is contained in:
Brandon Thomas
2026-10-04 00:15:57 -04:00
32 changed files with 3565 additions and 174 deletions
+4 -2
View File
@@ -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.
+36 -1
View File
@@ -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
+4 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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, &times)?;
let pm = PixelMotion::from(ctx, fr.grid.scale);
let warps = fetch_layer(ctx, "warpLayer", &times, &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, &times) else { return b };
let grid = fr.grid.clone();
let frame = |t: f64| -> &Image {
+36 -4
View File
@@ -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 {
+10
View File
@@ -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)| {
+4
View File
@@ -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),
}
}
+126
View File
@@ -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)
}
+34
View File
@@ -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 })
}
+223
View File
@@ -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, &times)
}
"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, &times) 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))
}
+595
View File
@@ -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
View File
@@ -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;
+21
View File
@@ -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);
}
+231
View File
@@ -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);
}
+59
View File
@@ -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);
}
+176
View File
@@ -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);
}
+883
View File
@@ -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);
}
+32
View File
@@ -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));
}
+88 -1
View File
@@ -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);
}
}
+56
View File
@@ -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))]);
}
+44
View File
@@ -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))]);
}
+116
View File
@@ -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"));
}
+157
View File
@@ -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
View File
@@ -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(())
}
+19
View File
@@ -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)> {
+103 -23
View File
@@ -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);
}
+55 -14
View File
@@ -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);
+1 -1
View File
@@ -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;