Merge branch 'worktree-agent-a8dea01e1a1d42946'
This commit is contained in:
commit
ebcb8c95ce
|
|
@ -749,6 +749,43 @@ fn time_ms(n: usize, mut f: impl FnMut()) -> f64 {
|
|||
median(&mut v)
|
||||
}
|
||||
|
||||
/// `bench --gpu`'s adjustment-layer comp: a copy of the first comp with a full-frame adjustment
|
||||
/// layer on top running Gaussian Blur, Levels, Vibrance and Glow (all GPU effects).
|
||||
fn adjustment_bench_comp(p: &mut effectcraft_engine::project::Project) -> Option<ItemId> {
|
||||
use effectcraft_engine::effects;
|
||||
use effectcraft_engine::keyframe::Value;
|
||||
use effectcraft_engine::project::build::{self, Ids};
|
||||
use effectcraft_engine::project::{ItemKind, LayerSource, Solid};
|
||||
let (_, base) = p.comps().next().map(|(id, c)| (*id, c.clone()))?;
|
||||
let mut comp = base.clone();
|
||||
let (w, h) = (comp.width, comp.height);
|
||||
let sid = p.add_item(
|
||||
"Adjustment (bench)",
|
||||
effectcraft_engine::color::Label::None,
|
||||
None,
|
||||
ItemKind::Solid(Solid { color: [1.0; 3], width: w, height: h, pixel_aspect: 1.0 }),
|
||||
);
|
||||
let mut l = build::layer(p, &comp, "Adjustment (bench)", LayerSource::Solid { item: sid }, (w, h), None);
|
||||
l.switches.adjustment = true;
|
||||
for (id, vals) in [
|
||||
("ec.blur.gaussian", vec![("blurriness", Value::Scalar(12.0))]),
|
||||
("ec.color.levels", vec![("gamma", Value::Scalar(1.3))]),
|
||||
("ec.color.vibrance", vec![("vibrance", Value::Scalar(40.0))]),
|
||||
("ec.stylize.glow", vec![("threshold", Value::Scalar(55.0))]),
|
||||
] {
|
||||
let spec = effects::find(id)?;
|
||||
let mut next = p.next_id;
|
||||
let mut g = effects::instantiate(spec, &mut Ids(&mut next), spec.name, [w as f64, h as f64]);
|
||||
p.next_id = next;
|
||||
for (k, v) in vals {
|
||||
g.prop_mut(k)?.value = v;
|
||||
}
|
||||
l.props.sub_mut("effects")?.children.push(g.into());
|
||||
}
|
||||
comp.layers.insert(0, l);
|
||||
Some(p.add_item("Adjustment Layers (bench)", effectcraft_engine::color::Label::None, None, ItemKind::Comp(comp.into())))
|
||||
}
|
||||
|
||||
/// `bench --gpu`: CPU vs GPU ms/frame for every comp (or `--comp`) at Full and Half. "cold"
|
||||
/// renders everything (no layer cache); "warm" reuses the layer cache (playback / scrubbing of
|
||||
/// unchanged layers: compositing cost); "viewer" is the GPU display path without readback.
|
||||
|
|
@ -756,19 +793,30 @@ fn bench_gpu(s: &Session, args: &Args) -> Result<(), Failure> {
|
|||
use effectcraft_render::Backend;
|
||||
let gpu = effectcraft_gpu::Gpu::headless().ok_or_else(|| Failure::Error("--gpu: no usable GPU adapter".into()))?;
|
||||
let n = args.num("--n")?.unwrap_or(10.0).max(1.0) as usize;
|
||||
// Every comp, plus an adjustment-layer comp built for the benchmark (the main comp under a
|
||||
// full-frame adjustment layer with a GPU effect stack).
|
||||
let mut project = (*s.project).clone();
|
||||
let comps: Vec<ItemId> = match args.opt("--comp") {
|
||||
Some(_) => s.active_comp_id().into_iter().collect(),
|
||||
None => s.project.comps().map(|(id, _)| *id).collect(),
|
||||
None => {
|
||||
let mut v: Vec<ItemId> = s.project.comps().map(|(id, _)| *id).collect();
|
||||
v.extend(adjustment_bench_comp(&mut project));
|
||||
v
|
||||
}
|
||||
};
|
||||
let project = &project;
|
||||
eprintln!("GPU: {} — median of {n} runs (ms/frame)", effectcraft_render::Accelerator::name(&gpu));
|
||||
eprintln!("{:<28} {:>5} {:>10} {:>9} {:>9} {:>9} {:>9} {:>9}", "comp", "res", "size", "cpu cold", "gpu cold", "cpu warm", "gpu warm", "gpu view");
|
||||
eprintln!(
|
||||
"{:<28} {:>5} {:>10} {:>9} {:>9} {:>9} {:>9} {:>9} {:>8} {:>9}",
|
||||
"comp", "res", "size", "cpu cold", "gpu cold", "cpu warm", "gpu warm", "gpu view", "speedup", "gpu≠cpu"
|
||||
);
|
||||
for cid in comps {
|
||||
let Some(comp) = s.project.comp(cid) else { continue };
|
||||
let name = s.project.item(cid).map(|i| i.name.clone()).unwrap_or_default();
|
||||
let Some(comp) = project.comp(cid) else { continue };
|
||||
let name = project.item(cid).map(|i| i.name.clone()).unwrap_or_default();
|
||||
let t = Tick::from_seconds_f64(args.num("--time")?.unwrap_or(3.0).min(comp.duration.seconds() * 0.5));
|
||||
for (label, scale) in [("Full", 1.0), ("Half", 0.5)] {
|
||||
let mk = |backend: Backend, cache: Option<&LayerCache>| {
|
||||
let mut r = Renderer::new(&s.project, s.footage.as_ref(), RenderOpts { scale, backend, ..Default::default() });
|
||||
let mut r = Renderer::new(project, s.footage.as_ref(), RenderOpts { scale, backend, ..Default::default() });
|
||||
r.expr = s.expr.as_deref();
|
||||
r.cache = cache;
|
||||
r.accel = Some(&gpu);
|
||||
|
|
@ -789,7 +837,7 @@ fn bench_gpu(s: &Session, args: &Args) -> Result<(), Failure> {
|
|||
std::hint::black_box(mk(Backend::Gpu, Some(&gcache)));
|
||||
});
|
||||
let view = time_ms(n, || {
|
||||
let mut r = Renderer::new(&s.project, s.footage.as_ref(), RenderOpts { scale, backend: Backend::Gpu, ..Default::default() });
|
||||
let mut r = Renderer::new(project, s.footage.as_ref(), RenderOpts { scale, backend: Backend::Gpu, ..Default::default() });
|
||||
r.expr = s.expr.as_deref();
|
||||
r.cache = Some(&gcache);
|
||||
r.accel = Some(&gpu);
|
||||
|
|
@ -797,7 +845,15 @@ fn bench_gpu(s: &Session, args: &Args) -> Result<(), Failure> {
|
|||
gpu.wait();
|
||||
});
|
||||
let size = format!("{}x{}", (comp.width as f64 * scale).round(), (comp.height as f64 * scale).round());
|
||||
eprintln!("{name:<28} {label:>5} {size:>10} {cpu_cold:>9.2} {gpu_cold:>9.2} {cpu_warm:>9.2} {gpu_warm:>9.2} {view:>9.2}");
|
||||
// Agreement: share of pixels where the GPU frame differs from the CPU reference by
|
||||
// more than 1/255 on any channel.
|
||||
let (a, b) = (mk(Backend::Cpu, Some(&cache)), mk(Backend::Gpu, Some(&gcache)));
|
||||
let off = a.data.iter().zip(&b.data).filter(|(p, q)| (0..4).any(|c| (p[c] - q[c]).abs() > 1.0 / 255.0 + 1e-6)).count();
|
||||
let pct = 100.0 * off as f64 / a.data.len().max(1) as f64;
|
||||
let speedup = cpu_warm / gpu_warm.max(1e-9);
|
||||
eprintln!(
|
||||
"{name:<28} {label:>5} {size:>10} {cpu_cold:>9.2} {gpu_cold:>9.2} {cpu_warm:>9.2} {gpu_warm:>9.2} {view:>9.2} {speedup:>7.2}x {pct:>8.3}%"
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
|
|
|||
|
|
@ -277,11 +277,9 @@ fn iris_spans(r: f64, sides: u32, rotation_deg: f64, roundness: f64) -> Vec<(i64
|
|||
spans
|
||||
}
|
||||
|
||||
fn camera_lens_blur(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let r = ctx.params.f("blurRadius").max(0.0) * b.scale;
|
||||
if r < 0.5 {
|
||||
return b;
|
||||
}
|
||||
/// Camera Lens Blur's iris spans at blur radius `r` pixels (shape, rotation, roundness and
|
||||
/// aspect ratio applied) and the iris' horizontal reach (shared with the GPU kernel).
|
||||
pub fn camera_lens_spans(ctx: &EffectCtx, r: f64) -> (Vec<(i64, i64, i64)>, f64) {
|
||||
let shape = ctx.params.e("irisProperties/irisShape");
|
||||
let sides = if shape >= 8 { 0 } else { shape + 3 };
|
||||
let mut spans = iris_spans(r, sides, ctx.params.f("irisProperties/irisRotation"), ctx.params.f("irisProperties/irisRoundness").clamp(0.0, 100.0) / 100.0);
|
||||
|
|
@ -293,7 +291,15 @@ fn camera_lens_blur(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
s.2 = (s.2 as f64 * aspect).round() as i64;
|
||||
}
|
||||
}
|
||||
let reach = r * aspect.max(1.0);
|
||||
(spans, r * aspect.max(1.0))
|
||||
}
|
||||
|
||||
fn camera_lens_blur(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let r = ctx.params.f("blurRadius").max(0.0) * b.scale;
|
||||
if r < 0.5 {
|
||||
return b;
|
||||
}
|
||||
let (spans, reach) = camera_lens_spans(ctx, r);
|
||||
let repeat = ctx.params.b("repeatEdge") || ctx.adjustment;
|
||||
if !repeat {
|
||||
b.pad(reach.ceil() as u32 + 1);
|
||||
|
|
|
|||
|
|
@ -414,7 +414,8 @@ fn grid_warp(src: &Image, w: u32, h: u32, nx: usize, ny: usize, dest: &[Pt], src
|
|||
out
|
||||
}
|
||||
|
||||
fn parse_mesh(s: &str) -> Vec<Pt> {
|
||||
/// Mesh Warp's `mesh` parameter: the vertex offsets (layer pixels), row-major.
|
||||
pub fn parse_mesh(s: &str) -> Vec<(f64, f64)> {
|
||||
s.split(|c: char| c.is_whitespace() || c == ';')
|
||||
.filter(|t| !t.is_empty())
|
||||
.filter_map(|t| {
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ pub(crate) const GEN_BLEND_MODES: [&str; 20] = [
|
|||
"Silhouette Alpha",
|
||||
];
|
||||
|
||||
fn gen_mode(i: u32) -> Option<BlendMode> {
|
||||
pub fn gen_mode(i: u32) -> Option<BlendMode> {
|
||||
Some(match i {
|
||||
0 => return None,
|
||||
2 => BlendMode::Add,
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ const CELL_PATTERNS: [&str; 12] = [
|
|||
];
|
||||
|
||||
/// Shaping used for Cell Pattern option `i` (see [`cell_value`]).
|
||||
fn pattern_kind(i: u32) -> u32 {
|
||||
pub fn pattern_kind(i: u32) -> u32 {
|
||||
match i {
|
||||
0..=5 => i,
|
||||
6 => 1,
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ pub mod ocio_config;
|
|||
pub mod paint;
|
||||
mod perspective;
|
||||
mod perspective2;
|
||||
pub mod psim;
|
||||
pub mod puppet;
|
||||
pub mod roto;
|
||||
mod sim;
|
||||
|
|
@ -63,11 +64,20 @@ use std::sync::OnceLock;
|
|||
|
||||
pub use color_fx::{exposure_settings, levels_clip};
|
||||
pub use color2::Curve;
|
||||
pub use distort2::parse_mesh;
|
||||
use effectcraft_keyframe::Value;
|
||||
use effectcraft_project::build::Ids;
|
||||
use effectcraft_project::{GroupKind, ParamUi, PropGroup, Property};
|
||||
pub use effectcraft_raster::{AuxChannels, Image};
|
||||
pub use misc::{INVERT_ALPHA, INVERT_CHANNELS};
|
||||
pub use sim::particle_state;
|
||||
pub use sim3::playground_state;
|
||||
// CPU helpers the GPU kernels share (effectcraft-gpu).
|
||||
pub use blur2::camera_lens_spans;
|
||||
pub use generate::gen_mode;
|
||||
pub use generate2::pattern_kind as cell_pattern_kind;
|
||||
pub use noise::GrainLook;
|
||||
pub use transition::place_layer;
|
||||
|
||||
/// Effect categories in Effects & Presets order.
|
||||
pub const CATEGORIES: &[&str] = &[
|
||||
|
|
@ -261,6 +271,11 @@ pub trait EffectHost: Sync {
|
|||
fn aux(&self) -> Option<std::sync::Arc<AuxChannels>> {
|
||||
None
|
||||
}
|
||||
/// A particle simulation backend (GPU particles; see [`psim`]). `None` = simulate on the
|
||||
/// CPU.
|
||||
fn particles(&self) -> Option<&dyn psim::ParticleSim> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Extra context the renderer may supply (all optional; `Default` is "nothing known").
|
||||
|
|
@ -642,6 +657,42 @@ pub const GPU_EFFECTS: &[&str] = &[
|
|||
"ec.color.exposure",
|
||||
"ec.channel.invert",
|
||||
"ec.distort.transform",
|
||||
"ec.distort.turbulentdisplace",
|
||||
"ec.distort.displacementmap",
|
||||
"ec.distort.wavewarp",
|
||||
"ec.distort.ripple",
|
||||
"ec.distort.twirl",
|
||||
"ec.distort.bulge",
|
||||
"ec.distort.cclens",
|
||||
"ec.distort.meshwarp",
|
||||
"ec.stylize.mosaic",
|
||||
"ec.stylize.findedges",
|
||||
"ec.stylize.emboss",
|
||||
// Blur, transition and generate family (effectcraft-gpu `fx_generate`).
|
||||
"ec.blur.radial",
|
||||
"ec.blur.cameralens",
|
||||
"ec.blur.ccradialfast",
|
||||
"ec.transition.venetian",
|
||||
"ec.transition.linearwipe",
|
||||
"ec.transition.radialwipe",
|
||||
"ec.transition.gradientwipe",
|
||||
"ec.generate.cellpattern",
|
||||
"ec.generate.checkerboard",
|
||||
"ec.generate.grid",
|
||||
"ec.generate.fourcolor",
|
||||
"ec.noise.noise",
|
||||
"ec.noise.addgrain",
|
||||
// effectcraft-gpu fx_color
|
||||
"ec.color.colorbalance",
|
||||
"ec.color.vibrance",
|
||||
"ec.color.lumetri",
|
||||
"ec.color.blackwhite",
|
||||
"ec.color.tritone",
|
||||
"ec.color.colorama",
|
||||
"ec.color.channelmixer",
|
||||
"ec.color.selectivecolor",
|
||||
"ec.key.linearcolor",
|
||||
"ec.keying.keylight",
|
||||
];
|
||||
|
||||
/// Effects whose output depends on [`EffectCtx::time`] directly (not only through animated
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ pub(crate) const GRAIN_BLEND_MODES: [&str; 5] = ["Film", "Multiply", "Add", "Scr
|
|||
|
||||
/// The Tweaking / Color / Application / Animation controls Add Grain and Match Grain share
|
||||
/// (twirl-down groups with the same ids in both effects).
|
||||
pub(crate) struct GrainLook {
|
||||
pub struct GrainLook {
|
||||
pub intensity: f32,
|
||||
pub channel_intensity: [f32; 3],
|
||||
pub size: f32,
|
||||
|
|
@ -62,7 +62,7 @@ pub(crate) struct GrainLook {
|
|||
}
|
||||
|
||||
impl GrainLook {
|
||||
pub(crate) fn from_params(ctx: &EffectCtx) -> GrainLook {
|
||||
pub fn from_params(ctx: &EffectCtx) -> GrainLook {
|
||||
let pr = ctx.params;
|
||||
let ch = |pre: &str, a: &str, b: &str, c: &str| [a, b, c].map(|k| pr.get(&format!("{pre}{k}")).map(Value::as_f64).unwrap_or(1.0) as f32);
|
||||
let ft = ctx.time * 24.0 * pr.f("animation/animationSpeed");
|
||||
|
|
|
|||
|
|
@ -0,0 +1,106 @@
|
|||
//! Pluggable particle simulation (GPU particles, M12.7).
|
||||
//!
|
||||
//! The stepped particle systems (CC Particle World, CC Particle Systems II and Particle
|
||||
//! Playground's Cannon without interacting forces) evolve every particle independently: births
|
||||
//! come from a rate (the same `carry` arithmetic as the CPU step), and each particle integrates
|
||||
//! its own motion with the same fixed step. An accelerator ([`ParticleSim`], reached through
|
||||
//! [`crate::EffectHost::particles`]) can therefore simulate all of them in parallel and return
|
||||
//! the state the CPU's `SimCache` would reach. The CPU stays the oracle: anything the backend
|
||||
//! declines (interacting forces, caps that would drop births) simulates on the CPU.
|
||||
|
||||
/// CC Particle World / CC Particle Systems II physics (see `sim::Phys`), with the per-step and
|
||||
/// per-spawn constants already evaluated on the CPU (bit-identical to the CPU's).
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct EngineDesc {
|
||||
pub life: f32,
|
||||
pub producer: [f32; 3],
|
||||
pub radius: [f32; 3],
|
||||
/// Animation (0 Explosive … 9 Fractal Uni).
|
||||
pub anim: u32,
|
||||
pub speed: f32,
|
||||
pub gravity: [f32; 3],
|
||||
/// Normalised direction axis.
|
||||
pub axis: [f32; 3],
|
||||
pub extra: f32,
|
||||
/// Cone Axis spread factor `|tan(extra angle / 2)|` (≤ 20).
|
||||
pub cone: f32,
|
||||
/// Velocity damping per step `exp(−drag · dt)`.
|
||||
pub damp: f32,
|
||||
pub seed: u32,
|
||||
/// 2D systems keep z = 0.
|
||||
pub flat: bool,
|
||||
}
|
||||
|
||||
/// Particle Playground's Cannon with gravity only (see `sim3::Pg`).
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct CannonDesc {
|
||||
pub pos: [f32; 2],
|
||||
pub barrel_angle: f32,
|
||||
pub barrel_radius: f32,
|
||||
pub dir_spread: f32,
|
||||
pub vel: f32,
|
||||
pub vel_spread: f32,
|
||||
pub gravity: [f32; 2],
|
||||
pub grav_spread: f32,
|
||||
pub seed: u32,
|
||||
/// Particles leaving `[x0, y0, x1, y1]` are removed.
|
||||
pub bounds: [f32; 4],
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum ParticleSystem {
|
||||
Engine(EngineDesc),
|
||||
Cannon(CannonDesc),
|
||||
}
|
||||
|
||||
/// One simulation request: the state after `steps` steps of system `key`.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SimRequest<'a> {
|
||||
/// Identifies the system (parameters, seed); checkpoints are reused per key.
|
||||
pub key: u64,
|
||||
pub steps: u64,
|
||||
/// Birth step of every particle id born before `steps` (ascending).
|
||||
pub births: &'a [u32],
|
||||
pub system: ParticleSystem,
|
||||
}
|
||||
|
||||
/// A live particle (ascending ids, as the CPU state keeps them).
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct SimParticle {
|
||||
pub p: [f32; 3],
|
||||
pub v: [f32; 3],
|
||||
pub age: f32,
|
||||
pub life: f32,
|
||||
pub rnd: f32,
|
||||
pub id: u32,
|
||||
}
|
||||
|
||||
/// A particle simulation backend (the GPU).
|
||||
pub trait ParticleSim: Send + Sync {
|
||||
/// The live particles after `req.steps` steps, or `None` (simulate on the CPU).
|
||||
fn simulate(&self, req: &SimRequest) -> Option<Vec<SimParticle>>;
|
||||
}
|
||||
|
||||
/// Birth step of every particle born in the first `steps` steps at `rate` particles per second
|
||||
/// (the CPU step's `carry` arithmetic, exactly). `None` when more than `cap` would be born (the
|
||||
/// CPU drops births at its live-particle cap, which an independent simulation can't know).
|
||||
pub fn births(rate: f64, steps: u64, cap: usize) -> Option<Vec<u32>> {
|
||||
let per = rate / SPS;
|
||||
if per * steps as f64 > cap as f64 + 2.0 {
|
||||
return None;
|
||||
}
|
||||
let mut out = Vec::new();
|
||||
let mut carry = 0.0f64;
|
||||
for i in 0..steps {
|
||||
carry += per;
|
||||
let n = carry.floor() as u32;
|
||||
carry -= n as f64;
|
||||
for _ in 0..n {
|
||||
out.push(i as u32);
|
||||
}
|
||||
}
|
||||
(out.len() <= cap).then_some(out)
|
||||
}
|
||||
|
||||
/// Steps per second of the stepped simulations.
|
||||
pub const SPS: f64 = crate::sim::SPS;
|
||||
|
|
@ -818,6 +818,60 @@ impl Phys {
|
|||
}
|
||||
}
|
||||
|
||||
impl Phys {
|
||||
/// The backend description (per-step and per-spawn constants evaluated here, in f32).
|
||||
pub(crate) fn desc(&self) -> crate::psim::EngineDesc {
|
||||
let mut drag = self.resistance;
|
||||
if self.anim == 3 {
|
||||
drag += 3.0 * self.extra.max(0.1);
|
||||
}
|
||||
let dt = (1.0 / SPS) as f32;
|
||||
crate::psim::EngineDesc {
|
||||
life: self.life,
|
||||
producer: self.producer,
|
||||
radius: self.radius,
|
||||
anim: self.anim,
|
||||
speed: self.speed,
|
||||
gravity: self.gravity,
|
||||
axis: norm3(self.axis),
|
||||
extra: self.extra,
|
||||
cone: (self.extra_angle.to_radians() * 0.5).tan().abs().min(20.0),
|
||||
damp: (-drag * dt).exp(),
|
||||
seed: self.seed,
|
||||
flat: self.flat,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The state at layer time `ctx.time` from the host's particle backend (GPU particles), or
|
||||
/// `None` (simulate on the CPU).
|
||||
fn accelerated(ctx: &EffectCtx, key: u64, phys: &Phys) -> Option<std::sync::Arc<PState>> {
|
||||
let backend = ctx.env.host?.particles()?;
|
||||
let steps = steps_at(ctx.time);
|
||||
let births = crate::psim::births(phys.rate, steps, phys.max)?;
|
||||
let req = crate::psim::SimRequest { key, steps, births: &births, system: crate::psim::ParticleSystem::Engine(phys.desc()) };
|
||||
let parts = backend.simulate(&req)?;
|
||||
Some(std::sync::Arc::new(PState {
|
||||
parts: parts.into_iter().map(|q| Particle { p: q.p, v: q.v, age: q.age, life: q.life, rnd: q.rnd, id: q.id }).collect(),
|
||||
carry: 0.0,
|
||||
next_id: births.len() as u32,
|
||||
}))
|
||||
}
|
||||
|
||||
/// The live particles of a stepped particle effect (`ec.sim.ccparticleworld`,
|
||||
/// `ec.sim.ccparticlesystems2`) at `ctx.time`: from the host's particle backend when it has
|
||||
/// one (GPU particles), else from the CPU simulation. For comparing the two.
|
||||
pub fn particle_state(id: &str, ctx: &EffectCtx) -> Option<Vec<crate::psim::SimParticle>> {
|
||||
let (phys, cache, salt) = match id {
|
||||
"ec.sim.ccparticleworld" => (pw_phys(ctx), &PW_CACHE, 1),
|
||||
"ec.sim.ccparticlesystems2" => (ps2_phys(ctx), &PS2_CACHE, 2),
|
||||
_ => return None,
|
||||
};
|
||||
let key = params_key(ctx, &Buf { img: Image::new(0, 0), offset: [0.0; 2], scale: 1.0 }, salt);
|
||||
let st = accelerated(ctx, key, &phys).unwrap_or_else(|| simulate(cache, key, ctx.time, &phys));
|
||||
Some(st.parts.iter().map(|q| crate::psim::SimParticle { p: q.p, v: q.v, age: q.age, life: q.life, rnd: q.rnd, id: q.id }).collect())
|
||||
}
|
||||
|
||||
/// Run a particle system to layer time `t` through `cache`.
|
||||
pub(crate) fn simulate(cache: &SimCache<PState>, key: u64, t: f64, phys: &Phys) -> std::sync::Arc<PState> {
|
||||
cache.run(key, steps_at(t), PState::default, |st, i| phys.step(st, i))
|
||||
|
|
@ -859,10 +913,10 @@ fn type_sprite(kind: u32, x: f32, y: f32, r: f32, c: [f32; 4], vx: f32, vy: f32,
|
|||
|
||||
static PW_CACHE: SimCache<PState> = SimCache::new(6);
|
||||
|
||||
fn particle_world(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
/// CC Particle World's physics.
|
||||
fn pw_phys(ctx: &EffectCtx) -> Phys {
|
||||
let pr = ctx.params;
|
||||
let (lw, lh) = (ctx.layer_size[0] as f32, ctx.layer_size[1] as f32);
|
||||
let phys = Phys {
|
||||
Phys {
|
||||
rate: pr.f("birthRate").max(0.0) * 120.0,
|
||||
life: pr.f("longevity").max(0.0) as f32,
|
||||
producer: [pr.f("producer/producerX") as f32, pr.f("producer/producerY") as f32, pr.f("producer/producerZ") as f32],
|
||||
|
|
@ -882,11 +936,18 @@ fn particle_world(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
seed: (pr.f("extras/randomSeed") as u32).wrapping_mul(0x9e3779b9) ^ ctx.seed,
|
||||
max: 40_000,
|
||||
flat: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn particle_world(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let pr = ctx.params;
|
||||
let (lw, lh) = (ctx.layer_size[0] as f32, ctx.layer_size[1] as f32);
|
||||
let phys = pw_phys(ctx);
|
||||
if phys.rate <= 0.0 && ctx.time <= 0.0 {
|
||||
return b;
|
||||
}
|
||||
let st = simulate(&PW_CACHE, params_key(ctx, &Buf { img: Image::new(0, 0), offset: [0.0; 2], scale: 1.0 }, 1), ctx.time, &phys);
|
||||
let key = params_key(ctx, &Buf { img: Image::new(0, 0), offset: [0.0; 2], scale: 1.0 }, 1);
|
||||
let st = accelerated(ctx, key, &phys).unwrap_or_else(|| simulate(&PW_CACHE, key, ctx.time, &phys));
|
||||
let kind = pr.e("particle/particleType");
|
||||
let birth = pr.color("particle/birthColor");
|
||||
let death = pr.color("particle/deathColor");
|
||||
|
|
@ -936,13 +997,14 @@ fn particle_world(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
|
||||
static PS2_CACHE: SimCache<PState> = SimCache::new(6);
|
||||
|
||||
fn particle_systems2(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
/// CC Particle Systems II's physics.
|
||||
fn ps2_phys(ctx: &EffectCtx) -> Phys {
|
||||
let pr = ctx.params;
|
||||
let lh = ctx.layer_size[1] as f32;
|
||||
let unit = lh.max(1.0);
|
||||
let pos = pr.v2("producer/position");
|
||||
let dir = (pr.f("physics/direction") as f32).to_radians();
|
||||
let phys = Phys {
|
||||
Phys {
|
||||
rate: pr.f("birthRate").max(0.0) * 60.0,
|
||||
life: pr.f("longevity").max(0.0) as f32,
|
||||
producer: [pos[0] as f32 / unit, pos[1] as f32 / unit, 0.0],
|
||||
|
|
@ -958,8 +1020,16 @@ fn particle_systems2(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
seed: (pr.f("randomSeed") as u32).wrapping_mul(0x85ebca6b) ^ ctx.seed,
|
||||
max: 40_000,
|
||||
flat: true,
|
||||
};
|
||||
let st = simulate(&PS2_CACHE, params_key(ctx, &Buf { img: Image::new(0, 0), offset: [0.0; 2], scale: 1.0 }, 2), ctx.time, &phys);
|
||||
}
|
||||
}
|
||||
|
||||
fn particle_systems2(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let pr = ctx.params;
|
||||
let lh = ctx.layer_size[1] as f32;
|
||||
let unit = lh.max(1.0);
|
||||
let phys = ps2_phys(ctx);
|
||||
let key = params_key(ctx, &Buf { img: Image::new(0, 0), offset: [0.0; 2], scale: 1.0 }, 2);
|
||||
let st = accelerated(ctx, key, &phys).unwrap_or_else(|| simulate(&PS2_CACHE, key, ctx.time, &phys));
|
||||
let kind = pr.e("particle/particleType");
|
||||
let birth = pr.color("particle/birthColor");
|
||||
let death = pr.color("particle/deathColor");
|
||||
|
|
|
|||
|
|
@ -231,6 +231,38 @@ impl Pg {
|
|||
|
||||
static PG_CACHE: SimCache<PgState> = SimCache::new(6);
|
||||
|
||||
/// Particle Playground cannons with gravity only, simulated by the host's particle backend.
|
||||
fn pg_accelerated(ctx: &EffectCtx, key: u64, pg: &Pg) -> Option<std::sync::Arc<PgState>> {
|
||||
let backend = ctx.env.host?.particles()?;
|
||||
let steps = steps_at(ctx.time);
|
||||
let births = crate::psim::births(pg.rate, steps, 60_000)?;
|
||||
let desc = crate::psim::CannonDesc {
|
||||
pos: pg.pos,
|
||||
barrel_angle: pg.barrel_angle,
|
||||
barrel_radius: pg.barrel_radius,
|
||||
dir_spread: pg.dir_spread,
|
||||
vel: pg.vel,
|
||||
vel_spread: pg.vel_spread,
|
||||
gravity: pg.gravity,
|
||||
grav_spread: pg.grav_spread,
|
||||
seed: pg.seed,
|
||||
bounds: pg.bounds,
|
||||
};
|
||||
let req = crate::psim::SimRequest { key, steps, births: &births, system: crate::psim::ParticleSystem::Cannon(desc) };
|
||||
let parts = backend.simulate(&req)?;
|
||||
Some(std::sync::Arc::new(PgState {
|
||||
parts: parts
|
||||
.into_iter()
|
||||
.map(|q| {
|
||||
let born = pg.spawn(q.id);
|
||||
PgParticle { p: [q.p[0], q.p[1]], v: [q.v[0], q.v[1]], ..born }
|
||||
})
|
||||
.collect(),
|
||||
carry: 0.0,
|
||||
next_id: births.len() as u32,
|
||||
}))
|
||||
}
|
||||
|
||||
fn image_key(img: &Image) -> u64 {
|
||||
use std::hash::{Hash, Hasher};
|
||||
let mut h = std::collections::hash_map::DefaultHasher::new();
|
||||
|
|
@ -244,14 +276,15 @@ fn image_key(img: &Image) -> u64 {
|
|||
h.finish()
|
||||
}
|
||||
|
||||
fn particle_playground(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
/// Particle Playground's producers and forces.
|
||||
fn pg_of(ctx: &EffectCtx) -> Pg {
|
||||
let pr = ctx.params;
|
||||
let (lw, lh) = (ctx.layer_size[0] as f32, ctx.layer_size[1] as f32);
|
||||
let grav_dir = (pr.f("gravity/gravityDirection") as f32).to_radians();
|
||||
let gf = pr.f("gravity/gravityForce") as f32;
|
||||
let wall = (pr.f("wall/wallBoundary").round() as usize).checked_sub(1).and_then(|i| ctx.env.masks.get(i)).map(|m| m.points.clone());
|
||||
let mapper = if pr.b("mapperEnabled") { ctx.layer_param("persistentPropertyMapper/useLayerAsMap", true).map(|lp| MapImg::from(ctx, lp)) } else { None };
|
||||
let pg = Pg {
|
||||
Pg {
|
||||
cannon: pr.b("cannonEnabled"),
|
||||
pos: { pr.v2("cannon/cannonPosition").map(|v| v as f32) },
|
||||
barrel_angle: pr.f("cannon/barrelAngle") as f32,
|
||||
|
|
@ -275,7 +308,34 @@ fn particle_playground(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
],
|
||||
seed: (pr.f("randomSeed") as u32).wrapping_mul(0x9e37_79b9) ^ ctx.seed,
|
||||
bounds: [-lw * 2.0, -lh * 2.0, lw * 3.0, lh * 3.0],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// The live particles of a Particle Playground whose cannon is the only producer and force
|
||||
/// besides gravity, at `ctx.time`: from the host's particle backend when it has one (GPU
|
||||
/// particles), else from the CPU simulation (`None` for other set-ups). For comparing the two.
|
||||
pub fn playground_state(ctx: &EffectCtx) -> Option<Vec<crate::psim::SimParticle>> {
|
||||
let pg = pg_of(ctx);
|
||||
let pr = ctx.params;
|
||||
let grid = pr.b("gridEnabled") && pr.f("grid/particlesAcross").round() > 0.0 && pr.f("grid/particlesDown").round() > 0.0;
|
||||
let exploder = pr.b("exploderEnabled") && pr.get("layerExploder/explodeLayer").and_then(Value::as_layer).is_some();
|
||||
if !pg.cannon || grid || exploder || (pg.repel != 0.0 && pg.repel_radius > 0.0) || pg.wall.is_some() || pg.mapper.is_some() {
|
||||
return None;
|
||||
}
|
||||
let key = params_key(ctx, &Buf { img: Image::new(0, 0), offset: [0.0; 2], scale: 1.0 }, 0x7067);
|
||||
let st = pg_accelerated(ctx, key, &pg).unwrap_or_else(|| PG_CACHE.run(key, steps_at(ctx.time), PgState::default, |st, _| pg.step(st)));
|
||||
Some(
|
||||
st.parts
|
||||
.iter()
|
||||
.map(|q| crate::psim::SimParticle { p: [q.p[0], q.p[1], 0.0], v: [q.v[0], q.v[1], 0.0], age: 0.0, life: 0.0, rnd: 0.0, id: q.id })
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
fn particle_playground(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let pr = ctx.params;
|
||||
let (lw, lh) = (ctx.layer_size[0] as f32, ctx.layer_size[1] as f32);
|
||||
let pg = pg_of(ctx);
|
||||
// Initial particles: Grid and Layer Exploder.
|
||||
let grid_on = pr.b("gridEnabled");
|
||||
let across = pr.f("grid/particlesAcross").max(0.0).round() as u32;
|
||||
|
|
@ -347,7 +407,15 @@ fn particle_playground(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
}
|
||||
st
|
||||
};
|
||||
let st = PG_CACHE.run(key, steps_at(ctx.time), init, |st, _| pg.step(st));
|
||||
// A cannon with gravity only: every particle moves on its own (GPU particles).
|
||||
let independent = pg.cannon
|
||||
&& !(grid_on && across > 0 && down > 0)
|
||||
&& exploder.is_none()
|
||||
&& !(pg.repel != 0.0 && pg.repel_radius > 0.0)
|
||||
&& pg.wall.is_none()
|
||||
&& pg.mapper.is_none();
|
||||
let gpu = if independent { pg_accelerated(ctx, key, &pg) } else { None };
|
||||
let st = gpu.unwrap_or_else(|| PG_CACHE.run(key, steps_at(ctx.time), init, |st, _| pg.step(st)));
|
||||
let s = b.scale as f32;
|
||||
let sprites: Vec<Sprite> = st
|
||||
.parts
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ fn scale_px(px: &mut Px, k: f32) {
|
|||
/// Resample `other` into `b`'s pixel grid with placement `mode` (Gradient Wipe's Gradient
|
||||
/// Placement, Texturize's Texture Placement): 0 tile from the top-left, 1 centre once,
|
||||
/// 2 stretch to fit.
|
||||
pub(crate) fn place_layer(ctx: &EffectCtx, b: &Buf, other: &crate::LayerPixels, mode: u32) -> effectcraft_raster::Image {
|
||||
pub fn place_layer(ctx: &EffectCtx, b: &Buf, other: &crate::LayerPixels, mode: u32) -> effectcraft_raster::Image {
|
||||
let (ls, os) = (ctx.layer_size, other.size);
|
||||
let inv = 1.0 / b.scale.max(1e-9);
|
||||
crate::util::gen_image(b.img.width, b.img.height, |x, y| {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,233 @@
|
|||
//! Classic 3D runs on the GPU (`classic3d.wgsl`): the CPU prepares the planes
|
||||
//! ([`Renderer::prepare_3d_run`](effectcraft_render::Renderer::prepare_3d_run): layer buffers,
|
||||
//! homographies, materials, lights, track mattes, bokeh depth of field), the GPU packs the
|
||||
//! buffers into one atlas texture and runs the per-pixel depth sort, lighting, ray-cast shadows
|
||||
//! and blending of `render::three_d::compose` in one kernel.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use effectcraft_geom::Mat4;
|
||||
use effectcraft_project::LightKind;
|
||||
use effectcraft_render::three_d::{LightState, PlaneGeo, Run3d};
|
||||
|
||||
use crate::context::{Enc, GpuImage, Params};
|
||||
use crate::ops::mode_id;
|
||||
|
||||
/// Fragments kept per pixel (`MAX_FRAGS` in the shader). Runs that could stack more planes on
|
||||
/// one pixel render on the CPU.
|
||||
const MAX_FRAGS: usize = 32;
|
||||
|
||||
/// Little-endian words for the shader's storage structs.
|
||||
#[derive(Default)]
|
||||
struct Words(Vec<u8>);
|
||||
|
||||
impl Words {
|
||||
fn f(&mut self, v: [f32; 4]) {
|
||||
for x in v {
|
||||
self.0.extend_from_slice(&x.to_le_bytes());
|
||||
}
|
||||
}
|
||||
fn u(&mut self, v: [u32; 4]) {
|
||||
for x in v {
|
||||
self.0.extend_from_slice(&x.to_le_bytes());
|
||||
}
|
||||
}
|
||||
fn d(&mut self, v: [f64; 4]) {
|
||||
self.f(v.map(|x| x as f32));
|
||||
}
|
||||
/// Pad an empty array to one element (`size` bytes) so its binding is valid.
|
||||
fn at_least(mut self, size: usize) -> Vec<u8> {
|
||||
if self.0.len() < size {
|
||||
self.0.resize(size, 0);
|
||||
}
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
fn affine_rows(w: &mut Words, m: &Mat4) {
|
||||
for r in 0..3 {
|
||||
w.d(m.0[r]);
|
||||
}
|
||||
}
|
||||
|
||||
/// Shelf-pack rectangles into an atlas no larger than `max` on a side: (width, height,
|
||||
/// positions).
|
||||
fn pack(sizes: &[(u32, u32)], max: u32) -> Option<(u32, u32, Vec<(u32, u32)>)> {
|
||||
let maxw = sizes.iter().map(|s| s.0).max()?;
|
||||
if maxw > max {
|
||||
return None;
|
||||
}
|
||||
let area: f64 = sizes.iter().map(|s| s.0 as f64 * s.1 as f64).sum();
|
||||
let limit = maxw.max((area.sqrt() * 1.25).ceil() as u32).min(max);
|
||||
let mut order: Vec<usize> = (0..sizes.len()).collect();
|
||||
order.sort_by_key(|&i| std::cmp::Reverse(sizes[i].1));
|
||||
let mut pos = vec![(0, 0); sizes.len()];
|
||||
let (mut x, mut y, mut shelf, mut width) = (0u32, 0u32, 0u32, 0u32);
|
||||
for i in order {
|
||||
let (w, h) = sizes[i];
|
||||
if x + w > limit {
|
||||
y += shelf;
|
||||
x = 0;
|
||||
shelf = 0;
|
||||
}
|
||||
pos[i] = (x, y);
|
||||
x += w;
|
||||
shelf = shelf.max(h);
|
||||
width = width.max(x);
|
||||
}
|
||||
let height = y + shelf;
|
||||
(height <= max).then_some((width.max(1), height.max(1), pos))
|
||||
}
|
||||
|
||||
/// Upper bound of the planes drawn on any one pixel: a pixel covered by a set of planes lies in
|
||||
/// each one's box, so every plane of the set overlaps the box of any member.
|
||||
fn max_overlap(boxes: &[[i64; 4]]) -> usize {
|
||||
boxes.iter().map(|a| boxes.iter().filter(|b| a[0] < b[2] && b[0] < a[2] && a[1] < b[3] && b[1] < a[3]).count()).max().unwrap_or(0)
|
||||
}
|
||||
|
||||
fn light_kind(k: LightKind) -> u32 {
|
||||
match k {
|
||||
LightKind::Ambient | LightKind::Environment => 0,
|
||||
LightKind::Parallel => 1,
|
||||
LightKind::Point => 2,
|
||||
LightKind::Spot => 3,
|
||||
}
|
||||
}
|
||||
|
||||
fn write_geo(w: &mut Words, g: &PlaneGeo) {
|
||||
// The homography is defined up to scale: normalise it (by a positive factor, keeping the
|
||||
// sign tests) into f32's comfortable range.
|
||||
let m = g.hinv.0.iter().flatten().fold(0.0f64, |a, v| a.max(v.abs()));
|
||||
let k = if m > 0.0 { 1.0 / m } else { 1.0 };
|
||||
for r in 0..3 {
|
||||
w.d([g.hinv.0[r][0] * k, g.hinv.0[r][1] * k, g.hinv.0[r][2] * k, 0.0]);
|
||||
}
|
||||
w.d([g.depth[0], g.depth[1], g.depth[2], 0.0]);
|
||||
affine_rows(w, &g.world);
|
||||
w.d([g.normal.x, g.normal.y, g.normal.z, 0.0]);
|
||||
w.d([g.cam.eye.x, g.cam.eye.y, g.cam.eye.z, if g.cam.ortho { 1.0 } else { 0.0 }]);
|
||||
let f = g.cam.forward();
|
||||
w.d([f.x, f.y, f.z, 0.0]);
|
||||
}
|
||||
|
||||
fn write_light(w: &mut Words, l: &LightState) {
|
||||
w.u([light_kind(l.kind), l.falloff, l.casts_shadows as u32, 0]);
|
||||
w.d([l.pos.x, l.pos.y, l.pos.z, 0.0]);
|
||||
w.d([l.dir.x, l.dir.y, l.dir.z, 0.0]);
|
||||
w.f([l.color[0], l.color[1], l.color[2], 0.0]);
|
||||
w.d([l.cone_half, l.feather, l.radius, l.falloff_distance]);
|
||||
w.d([l.shadow_darkness, l.shadow_diffusion, 0.0, 0.0]);
|
||||
}
|
||||
|
||||
const PLANE_BYTES: usize = 12 * 16;
|
||||
const GEO_BYTES: usize = 10 * 16;
|
||||
const LIGHT_BYTES: usize = 6 * 16;
|
||||
|
||||
/// Composite a prepared Classic 3D run over `canvas`. `None` when it cannot run here (the
|
||||
/// caller draws it on the CPU).
|
||||
pub(crate) fn draw_run(e: &mut Enc, run: &Run3d, canvas: &GpuImage) -> Option<GpuImage> {
|
||||
let (w, h) = (canvas.width, canvas.height);
|
||||
let boxes: Vec<[i64; 4]> = run.planes.iter().filter(|p| p.draw).map(|p| p.bbox).collect();
|
||||
if boxes.is_empty() {
|
||||
return Some(canvas.clone());
|
||||
}
|
||||
if max_overlap(&boxes) > MAX_FRAGS
|
||||
|| run.planes.iter().any(|p| p.winv.0[3] != [0.0, 0.0, 0.0, 1.0] || p.geos.iter().any(|g| g.world.0[3] != [0.0, 0.0, 0.0, 1.0]))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
// Plane textures: the layer uploads (cached per buffer; bokeh depth of field is already in
|
||||
// them).
|
||||
let mut texs: Vec<(GpuImage, [f64; 2])> = Vec::with_capacity(run.planes.len());
|
||||
for p in &run.planes {
|
||||
texs.push((e.g.upload_buf(&p.buf)?, p.buf.offset));
|
||||
}
|
||||
let sizes: Vec<(u32, u32)> = texs.iter().map(|(t, _)| (t.width, t.height)).collect();
|
||||
let (aw, ah, pos) = pack(&sizes, e.g.max_dim)?;
|
||||
if (aw as u64) * (ah as u64) * 16 > 2 << 30 {
|
||||
return None;
|
||||
}
|
||||
let atlas = e.image(aw, ah);
|
||||
for ((t, _), &(x, y)) in texs.iter().zip(&pos) {
|
||||
e.copy_into(t, &atlas, x, y);
|
||||
}
|
||||
// Track mattes (shared by the planes of one layer).
|
||||
let mut mattes: Vec<f32> = vec![];
|
||||
let mut matte_ix: HashMap<usize, u32> = HashMap::new();
|
||||
let n_px = w as usize * h as usize;
|
||||
let mut planes = Words::default();
|
||||
let mut geos = Words::default();
|
||||
let mut n_geos = 0u32;
|
||||
for ((p, (t, off)), &(ax, ay)) in run.planes.iter().zip(&texs).zip(&pos) {
|
||||
let matte = match &p.matte {
|
||||
Some(m) if m.len() == n_px => {
|
||||
let key = std::sync::Arc::as_ptr(m) as usize;
|
||||
let next = matte_ix.len() as u32 + 1;
|
||||
let ix = *matte_ix.entry(key).or_insert(next);
|
||||
if ix == next {
|
||||
mattes.extend_from_slice(m);
|
||||
}
|
||||
ix
|
||||
}
|
||||
Some(_) => return None,
|
||||
None => 0,
|
||||
};
|
||||
planes.f([ax as f32, ay as f32, t.width as f32, t.height as f32]);
|
||||
planes.d([off[0], off[1], p.buf.scale, if p.bicubic { 1.0 } else { 0.0 }]);
|
||||
planes.d(p.bbox.map(|v| v as f64));
|
||||
planes.d(p.bounds);
|
||||
affine_rows(&mut planes, &p.winv);
|
||||
planes.f([p.mat.ambient, p.mat.diffuse, p.mat.specular, p.mat.exponent() as f32]);
|
||||
planes.f([p.mat.metal, p.mat.light_transmission, p.opacity, 0.0]);
|
||||
let flags = p.draw as u32 | (p.preserve as u32) << 1 | (p.mat.accepts_lights as u32) << 2;
|
||||
planes.u([n_geos, p.geos.len() as u32, flags, mode_id(p.mode)]);
|
||||
planes.u([p.mat.casts_shadows, p.mat.accepts_shadows, p.layer_id, p.order as u32]);
|
||||
planes.u([matte, run.seed ^ p.layer_id, 0, 0]);
|
||||
for g in &p.geos {
|
||||
write_geo(&mut geos, g);
|
||||
}
|
||||
n_geos += p.geos.len() as u32;
|
||||
}
|
||||
if (mattes.len() * 4) as u64 > e.g.device.limits().max_storage_buffer_binding_size {
|
||||
return None;
|
||||
}
|
||||
let mut lights = Words::default();
|
||||
for l in &run.lights {
|
||||
write_light(&mut lights, l);
|
||||
}
|
||||
let mut casters = Words::default();
|
||||
for c in &run.casters {
|
||||
casters.0.extend_from_slice(&(*c as u32).to_le_bytes());
|
||||
}
|
||||
let pb = e.bytes(planes.at_least(PLANE_BYTES));
|
||||
let gb = e.bytes(geos.at_least(GEO_BYTES));
|
||||
let lb = e.bytes(lights.at_least(LIGHT_BYTES));
|
||||
let cb = e.bytes(casters.at_least(16));
|
||||
let mb = e.data(&mattes);
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [run.planes.len() as u32, run.lights.len() as u32, run.casters.len() as u32, 0];
|
||||
let out = e.image(w, h);
|
||||
e.dispatch_ext("classic3d", &p, &atlas, Some(canvas), &out, Some(&mb), (w.div_ceil(16), h.div_ceil(16)), Some([&pb, &gb, &lb, &cb]));
|
||||
Some(out)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn shelf_packing_fits_and_does_not_overlap() {
|
||||
let sizes = [(100, 40), (30, 90), (60, 60), (100, 10), (5, 5)];
|
||||
let (w, h, pos) = pack(&sizes, 256).unwrap();
|
||||
for (i, (&(x, y), &(sw, sh))) in pos.iter().zip(&sizes).enumerate() {
|
||||
assert!(x + sw <= w && y + sh <= h);
|
||||
for (j, (&(x2, y2), &(sw2, sh2))) in pos.iter().zip(&sizes).enumerate() {
|
||||
if i != j {
|
||||
assert!(x + sw <= x2 || x2 + sw2 <= x || y + sh <= y2 || y2 + sh2 <= y, "{i} and {j} overlap");
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(pack(&[(300, 10)], 256).is_none());
|
||||
assert_eq!(max_overlap(&[[0, 0, 10, 10], [5, 5, 15, 15], [20, 20, 30, 30]]), 2);
|
||||
}
|
||||
}
|
||||
|
|
@ -26,8 +26,13 @@ const ENTRIES: &[&str] = &[
|
|||
"shadow_make",
|
||||
"shadow_combine",
|
||||
"pointwise",
|
||||
"adjust_mix",
|
||||
];
|
||||
|
||||
/// Entry points that also bind group 1 (four read-only storage buffers; see
|
||||
/// [`Enc::dispatch_ext`]).
|
||||
const EXT_ENTRIES: &[&str] = &["classic3d"];
|
||||
|
||||
/// Pixel format of every working texture: premultiplied RGBA, 32-bit float (32 bpc headroom;
|
||||
/// 8/16 bpc are emulated by clamping and quantising, as on the CPU).
|
||||
pub const FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba32Float;
|
||||
|
|
@ -88,6 +93,7 @@ pub struct GpuContext {
|
|||
pub(crate) name: String,
|
||||
pub(crate) max_dim: u32,
|
||||
bgl: wgpu::BindGroupLayout,
|
||||
bgl_ext: wgpu::BindGroupLayout,
|
||||
pipelines: HashMap<&'static str, wgpu::ComputePipeline>,
|
||||
display_bgl: wgpu::BindGroupLayout,
|
||||
display: wgpu::ComputePipeline,
|
||||
|
|
@ -96,6 +102,9 @@ pub struct GpuContext {
|
|||
uploads: Mutex<Uploads>,
|
||||
/// Advanced 3D render pipelines (built on first use).
|
||||
pub(crate) adv3d: std::sync::OnceLock<crate::adv3d::Pipes>,
|
||||
/// GPU particle pipeline (built on first use) and simulation checkpoints.
|
||||
pub(crate) particles: crate::particles::PipesCell,
|
||||
pub(crate) particle_states: crate::particles::StatesCell,
|
||||
}
|
||||
|
||||
fn tex_entry(binding: u32) -> wgpu::BindGroupLayoutEntry {
|
||||
|
|
@ -135,7 +144,15 @@ impl GpuContext {
|
|||
}
|
||||
}
|
||||
let name = format!("{} ({:?})", info.name, info.backend);
|
||||
let src = [include_str!("shaders/common.wgsl"), include_str!("shaders/kernels.wgsl")].concat();
|
||||
let src = [
|
||||
include_str!("shaders/common.wgsl"),
|
||||
include_str!("shaders/kernels.wgsl"),
|
||||
include_str!("shaders/classic3d.wgsl"),
|
||||
include_str!("shaders/fx_color.wgsl"),
|
||||
include_str!("shaders/fx_distort.wgsl"),
|
||||
include_str!("shaders/fx_generate.wgsl"),
|
||||
]
|
||||
.concat();
|
||||
let module =
|
||||
device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("effectcraft kernels"), source: wgpu::ShaderSource::Wgsl(src.into()) });
|
||||
let bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
|
|
@ -167,12 +184,32 @@ impl GpuContext {
|
|||
bind_group_layouts: &[Some(&bgl)],
|
||||
immediate_size: 0,
|
||||
});
|
||||
let storage = |binding: u32| wgpu::BindGroupLayoutEntry {
|
||||
binding,
|
||||
visibility: wgpu::ShaderStages::COMPUTE,
|
||||
ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Storage { read_only: true }, has_dynamic_offset: false, min_binding_size: None },
|
||||
count: None,
|
||||
};
|
||||
let bgl_ext = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("effectcraft kernels (ext)"),
|
||||
entries: &[storage(0), storage(1), storage(2), storage(3)],
|
||||
});
|
||||
let layout_ext = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("effectcraft kernels (ext)"),
|
||||
bind_group_layouts: &[Some(&bgl), Some(&bgl_ext)],
|
||||
immediate_size: 0,
|
||||
});
|
||||
let pipelines = ENTRIES
|
||||
.iter()
|
||||
.map(|e| {
|
||||
.chain(crate::fx_color::KERNELS)
|
||||
.chain(crate::fx_distort::KERNELS)
|
||||
.chain(crate::fx_generate::KERNELS)
|
||||
.map(|e| (e, &layout))
|
||||
.chain(EXT_ENTRIES.iter().map(|e| (e, &layout_ext)))
|
||||
.map(|(e, layout)| {
|
||||
let p = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
|
||||
label: Some(e),
|
||||
layout: Some(&layout),
|
||||
layout: Some(layout),
|
||||
module: &module,
|
||||
entry_point: Some(e),
|
||||
compilation_options: Default::default(),
|
||||
|
|
@ -221,6 +258,7 @@ impl GpuContext {
|
|||
name,
|
||||
max_dim,
|
||||
bgl,
|
||||
bgl_ext,
|
||||
pipelines,
|
||||
display_bgl,
|
||||
display,
|
||||
|
|
@ -228,6 +266,8 @@ impl GpuContext {
|
|||
dummy_buf,
|
||||
uploads: Mutex::new(Uploads::default()),
|
||||
adv3d: std::sync::OnceLock::new(),
|
||||
particles: Default::default(),
|
||||
particle_states: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -374,6 +414,22 @@ impl<'g> Enc<'g> {
|
|||
out: &GpuImage,
|
||||
data: Option<&wgpu::Buffer>,
|
||||
groups: (u32, u32),
|
||||
) {
|
||||
self.dispatch_ext(entry, p, src, aux, out, data, groups, None);
|
||||
}
|
||||
|
||||
/// [`Enc::dispatch`] for the kernels in `EXT_ENTRIES`, with their group 1 storage buffers.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn dispatch_ext(
|
||||
&mut self,
|
||||
entry: &str,
|
||||
p: &Params,
|
||||
src: &GpuImage,
|
||||
aux: Option<&GpuImage>,
|
||||
out: &GpuImage,
|
||||
data: Option<&wgpu::Buffer>,
|
||||
groups: (u32, u32),
|
||||
ext: Option<[&wgpu::Buffer; 4]>,
|
||||
) {
|
||||
let g = self.g;
|
||||
let Some(pipe) = g.pipelines.get(entry) else {
|
||||
|
|
@ -395,11 +451,26 @@ impl<'g> Enc<'g> {
|
|||
wgpu::BindGroupEntry { binding: 4, resource: data.unwrap_or(&g.dummy_buf).as_entire_binding() },
|
||||
],
|
||||
});
|
||||
let bg_ext = ext.map(|b| {
|
||||
g.device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: None,
|
||||
layout: &g.bgl_ext,
|
||||
entries: &[
|
||||
wgpu::BindGroupEntry { binding: 0, resource: b[0].as_entire_binding() },
|
||||
wgpu::BindGroupEntry { binding: 1, resource: b[1].as_entire_binding() },
|
||||
wgpu::BindGroupEntry { binding: 2, resource: b[2].as_entire_binding() },
|
||||
wgpu::BindGroupEntry { binding: 3, resource: b[3].as_entire_binding() },
|
||||
],
|
||||
})
|
||||
});
|
||||
let enc = self.encoder();
|
||||
{
|
||||
let mut pass = enc.begin_compute_pass(&wgpu::ComputePassDescriptor { label: Some(entry), timestamp_writes: None });
|
||||
pass.set_pipeline(pipe);
|
||||
pass.set_bind_group(0, &bg, &[]);
|
||||
if let Some(b) = &bg_ext {
|
||||
pass.set_bind_group(1, b, &[]);
|
||||
}
|
||||
pass.dispatch_workgroups(groups.0.max(1), groups.1.max(1), 1);
|
||||
}
|
||||
self.pending += 1;
|
||||
|
|
@ -525,7 +596,33 @@ impl<'g> Enc<'g> {
|
|||
for x in v {
|
||||
bytes.extend_from_slice(&x.to_le_bytes());
|
||||
}
|
||||
while bytes.len() < 16 {
|
||||
self.bytes(bytes)
|
||||
}
|
||||
|
||||
/// Copy an image into a storage buffer (RGBA f32 rows) for kernels that read a third image
|
||||
/// through `data`: (buffer, row length in pixels).
|
||||
pub fn image_rows(&mut self, img: &GpuImage) -> (wgpu::Buffer, u32) {
|
||||
let row = (img.width * 16).div_ceil(wgpu::COPY_BYTES_PER_ROW_ALIGNMENT) * wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
|
||||
let buf = self.g.device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("image rows"),
|
||||
size: row as u64 * img.height as u64,
|
||||
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
self.encoder().copy_texture_to_buffer(
|
||||
wgpu::TexelCopyTextureInfo { texture: &img.texture, mip_level: 0, origin: wgpu::Origin3d::ZERO, aspect: wgpu::TextureAspect::All },
|
||||
wgpu::TexelCopyBufferInfo {
|
||||
buffer: &buf,
|
||||
layout: wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(row), rows_per_image: Some(img.height) },
|
||||
},
|
||||
wgpu::Extent3d { width: img.width, height: img.height, depth_or_array_layers: 1 },
|
||||
);
|
||||
(buf, row / 16)
|
||||
}
|
||||
|
||||
/// A read-only storage buffer holding `bytes` (padded to 16 bytes).
|
||||
pub fn bytes(&self, mut bytes: Vec<u8>) -> wgpu::Buffer {
|
||||
while bytes.len() < 16 || !bytes.len().is_multiple_of(4) {
|
||||
bytes.push(0);
|
||||
}
|
||||
self.g.device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("data"), contents: &bytes, usage: wgpu::BufferUsages::STORAGE })
|
||||
|
|
|
|||
|
|
@ -16,19 +16,19 @@ pub fn supports(id: &str) -> bool {
|
|||
}
|
||||
|
||||
/// A layer buffer on the GPU (see [`Buf`]).
|
||||
struct GBuf {
|
||||
img: GpuImage,
|
||||
offset: [f64; 2],
|
||||
scale: f64,
|
||||
pub(crate) struct GBuf {
|
||||
pub img: GpuImage,
|
||||
pub offset: [f64; 2],
|
||||
pub scale: f64,
|
||||
}
|
||||
|
||||
impl GBuf {
|
||||
fn to_px(&self, p: [f64; 2]) -> (f64, f64) {
|
||||
pub fn to_px(&self, p: [f64; 2]) -> (f64, f64) {
|
||||
(p[0] * self.scale + self.offset[0], p[1] * self.scale + self.offset[1])
|
||||
}
|
||||
|
||||
/// Buf::pad: grow by `pad` transparent pixels on every side.
|
||||
fn pad(&mut self, e: &mut Enc, pad: u32) -> Option<()> {
|
||||
pub fn pad(&mut self, e: &mut Enc, pad: u32) -> Option<()> {
|
||||
if pad == 0 {
|
||||
return Some(());
|
||||
}
|
||||
|
|
@ -62,6 +62,48 @@ pub(crate) fn run_chain(e: &mut Enc, chain: &[FxStep], buf: &Buf, levels: Option
|
|||
Some(Buf { img, offset: b.offset, scale: b.scale })
|
||||
}
|
||||
|
||||
/// An effect stack running on a GPU-resident image (adjustment layers): GPU chains stay on
|
||||
/// the device; CPU effects read the image back, run, and upload the result.
|
||||
pub(crate) struct GpuFx<'e, 'g> {
|
||||
e: &'e mut Enc<'g>,
|
||||
b: Option<GBuf>,
|
||||
levels: Option<f32>,
|
||||
}
|
||||
|
||||
impl<'e, 'g> GpuFx<'e, 'g> {
|
||||
pub fn new(e: &'e mut Enc<'g>, img: GpuImage, offset: [f64; 2], scale: f64, levels: Option<f32>) -> Self {
|
||||
GpuFx { e, b: Some(GBuf { img, offset, scale }), levels }
|
||||
}
|
||||
|
||||
/// The result: (image, offset, scale); `None` when a step failed.
|
||||
pub fn finish(self) -> Option<(GpuImage, [f64; 2], f64)> {
|
||||
self.b.map(|b| (b.img, b.offset, b.scale))
|
||||
}
|
||||
}
|
||||
|
||||
impl effectcraft_render::FxTarget for GpuFx<'_, '_> {
|
||||
fn gpu(&mut self, steps: &[FxStep]) -> bool {
|
||||
let Some(b) = &self.b else { return false };
|
||||
let mut cur = GBuf { img: b.img.clone(), offset: b.offset, scale: b.scale };
|
||||
for step in steps {
|
||||
let Some(next) = apply(self.e, step.spec.id, &step.ctx, cur) else { return false };
|
||||
cur = next;
|
||||
if let Some(l) = self.levels {
|
||||
cur.img = ops::quantize(self.e, &cur.img, l);
|
||||
}
|
||||
}
|
||||
self.b = Some(cur);
|
||||
true
|
||||
}
|
||||
|
||||
fn cpu(&mut self, f: &mut dyn FnMut(Buf) -> Buf) {
|
||||
let Some(b) = self.b.take() else { return };
|
||||
let Some(img) = self.e.download(&b.img) else { return };
|
||||
let out = f(Buf { img, offset: b.offset, scale: b.scale });
|
||||
self.b = self.e.g.upload_image(&out.img).map(|img| GBuf { img, offset: out.offset, scale: out.scale });
|
||||
}
|
||||
}
|
||||
|
||||
fn apply(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
match id {
|
||||
"ec.blur.gaussian" => gaussian(e, ctx, b),
|
||||
|
|
@ -72,6 +114,9 @@ fn apply(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
|||
"ec.distort.transform" => transform(e, ctx, b),
|
||||
"ec.color.curves" => curves(e, ctx, b),
|
||||
_ if id.starts_with("ec.control.") => Some(b),
|
||||
_ if crate::fx_color::IDS.contains(&id) => crate::fx_color::apply(e, id, ctx, b),
|
||||
_ if crate::fx_distort::IDS.contains(&id) => crate::fx_distort::apply(e, id, ctx, b),
|
||||
_ if crate::fx_generate::IDS.contains(&id) => crate::fx_generate::apply(e, id, ctx, b),
|
||||
_ => pointwise(e, id, ctx, b),
|
||||
}
|
||||
}
|
||||
|
|
@ -79,7 +124,7 @@ fn apply(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
|||
// ---------------------------------------------------------------- blurs
|
||||
|
||||
/// raster::blur::box_radii.
|
||||
fn box_radii(sigma: f64, n: usize) -> Vec<usize> {
|
||||
pub(crate) fn box_radii(sigma: f64, n: usize) -> Vec<usize> {
|
||||
if sigma <= 0.0 {
|
||||
return vec![0; n];
|
||||
}
|
||||
|
|
@ -95,7 +140,7 @@ fn box_radii(sigma: f64, n: usize) -> Vec<usize> {
|
|||
}
|
||||
|
||||
/// Horizontal passes with radii `rx`, then vertical passes with radii `ry` (raster::blur).
|
||||
fn box_passes(e: &mut Enc, img: &GpuImage, rx: &[usize], ry: &[usize], repeat: bool) -> GpuImage {
|
||||
pub(crate) fn box_passes(e: &mut Enc, img: &GpuImage, rx: &[usize], ry: &[usize], repeat: bool) -> GpuImage {
|
||||
let mut cur = img.clone();
|
||||
let passes = rx.iter().map(|&r| (false, r)).chain(ry.iter().map(|&r| (true, r))).filter(|p| p.1 > 0);
|
||||
for (vertical, r) in passes {
|
||||
|
|
@ -111,7 +156,7 @@ fn box_passes(e: &mut Enc, img: &GpuImage, rx: &[usize], ry: &[usize], repeat: b
|
|||
}
|
||||
|
||||
/// raster::gaussian_blur (3 box passes per axis).
|
||||
fn gaussian_blur(e: &mut Enc, img: &GpuImage, sx: f64, sy: f64, repeat: bool) -> GpuImage {
|
||||
pub(crate) fn gaussian_blur(e: &mut Enc, img: &GpuImage, sx: f64, sy: f64, repeat: bool) -> GpuImage {
|
||||
let rx = if sx > 0.05 { box_radii(sx, 3) } else { vec![] };
|
||||
let ry = if sy > 0.05 { box_radii(sy, 3) } else { vec![] };
|
||||
box_passes(e, img, &rx, &ry, repeat)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,396 @@
|
|||
//! GPU effects, colour family (kernels in `shaders/fx_color.wgsl`): the CPU effects' exact
|
||||
//! steps with the pixel loops as compute kernels.
|
||||
//!
|
||||
//! Colour Balance, Vibrance, Black & White, Tritone, Colorama, Channel Mixer, Selective Color
|
||||
//! and Linear Color Key are one per-pixel pass (`fxc_point`); Lumetri Color is a per-pixel pass
|
||||
//! plus, with Sharpen, a luminance plane blurred like `util::gauss_plane`; Key Light runs its
|
||||
//! matte pipeline (pre-blur, clip, rollback, despot, shrink / grow, softness, edge band) as
|
||||
//! plane passes. Key Light with an Inside / Outside mask, or Hard Colour replacement after
|
||||
//! Screen Softness, and Colorama with Interpolate Palette off run on the CPU.
|
||||
|
||||
use effectcraft_color::{luminance, rgb_to_hsl};
|
||||
use effectcraft_effects::EffectCtx;
|
||||
|
||||
use crate::context::{Enc, GpuImage, Params};
|
||||
use crate::effects::{GBuf, gaussian_blur};
|
||||
|
||||
/// Compute entry points in `fx_color.wgsl`.
|
||||
pub(crate) const KERNELS: &[&str] = &[
|
||||
"fxc_point",
|
||||
"fxc_lumetri",
|
||||
"fxc_luma",
|
||||
"fxc_sharpen",
|
||||
"fxc_morph",
|
||||
"fxc_lerp",
|
||||
"fxc_kl_raw",
|
||||
"fxc_kl_rollback",
|
||||
"fxc_kl_pack",
|
||||
"fxc_kl_band",
|
||||
"fxc_kl_edge",
|
||||
"fxc_kl_final",
|
||||
];
|
||||
|
||||
/// Effect ids implemented here.
|
||||
pub(crate) const IDS: &[&str] = &[
|
||||
"ec.color.colorbalance",
|
||||
"ec.color.vibrance",
|
||||
"ec.color.lumetri",
|
||||
"ec.color.blackwhite",
|
||||
"ec.color.tritone",
|
||||
"ec.color.colorama",
|
||||
"ec.color.channelmixer",
|
||||
"ec.color.selectivecolor",
|
||||
"ec.key.linearcolor",
|
||||
"ec.keying.keylight",
|
||||
];
|
||||
|
||||
/// 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.color.lumetri" => lumetri(e, ctx, b),
|
||||
"ec.keying.keylight" => key_light(e, ctx, b),
|
||||
_ => point(e, id, ctx, b),
|
||||
}
|
||||
}
|
||||
|
||||
fn rgb(c: [f32; 4]) -> [f32; 4] {
|
||||
[c[0], c[1], c[2], 0.0]
|
||||
}
|
||||
|
||||
/// Same selective colour ids as `color2::SC_IDS`.
|
||||
const SC_IDS: [[&str; 4]; 9] = [
|
||||
["redsCyan", "redsMagenta", "redsYellow", "redsBlack"],
|
||||
["yellowsCyan", "yellowsMagenta", "yellowsYellow", "yellowsBlack"],
|
||||
["greensCyan", "greensMagenta", "greensYellow", "greensBlack"],
|
||||
["cyansCyan", "cyansMagenta", "cyansYellow", "cyansBlack"],
|
||||
["bluesCyan", "bluesMagenta", "bluesYellow", "bluesBlack"],
|
||||
["magentasCyan", "magentasMagenta", "magentasYellow", "magentasBlack"],
|
||||
["whitesCyan", "whitesMagenta", "whitesYellow", "whitesBlack"],
|
||||
["neutralsCyan", "neutralsMagenta", "neutralsYellow", "neutralsBlack"],
|
||||
["blacksCyan", "blacksMagenta", "blacksYellow", "blacksBlack"],
|
||||
];
|
||||
|
||||
fn point(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let pr = ctx.params;
|
||||
let f = |k: &str| pr.f(k) as f32;
|
||||
let mut p = Params::default();
|
||||
let mut data = None;
|
||||
match id {
|
||||
"ec.color.colorbalance" => {
|
||||
let g = |k: &str| f(k) / 100.0;
|
||||
p.u[0] = [1, pr.b("preserveLuminosity") as u32, 0, 0];
|
||||
p.f[0] = [g("shadowRed"), g("shadowGreen"), g("shadowBlue"), 0.0];
|
||||
p.f[1] = [g("midRed"), g("midGreen"), g("midBlue"), 0.0];
|
||||
p.f[2] = [g("hiRed"), g("hiGreen"), g("hiBlue"), 0.0];
|
||||
}
|
||||
"ec.color.vibrance" => {
|
||||
p.u[0][0] = 2;
|
||||
p.f[0] = [f("vibrance") / 100.0, f("saturation") / 100.0, 0.0, 0.0];
|
||||
}
|
||||
"ec.color.blackwhite" => {
|
||||
let w = ["reds", "yellows", "greens", "cyans", "blues", "magentas"].map(|k| f(k) / 100.0);
|
||||
let tc = pr.color("tintColor");
|
||||
let (th, ts, _) = rgb_to_hsl(tc[0], tc[1], tc[2]);
|
||||
p.u[0] = [3, pr.b("tint") as u32, 0, 0];
|
||||
p.f[0] = [w[0], w[1], w[2], w[3]];
|
||||
p.f[1] = [w[4], w[5], 0.0, 0.0];
|
||||
p.f[2] = [th, ts, 0.0, 0.0];
|
||||
}
|
||||
"ec.color.tritone" => {
|
||||
p.u[0][0] = 4;
|
||||
p.f[0] = rgb(pr.color("highlights"));
|
||||
p.f[1] = rgb(pr.color("midtones"));
|
||||
p.f[2] = rgb(pr.color("shadows"));
|
||||
p.f[3][0] = 1.0 - f("blend") / 100.0;
|
||||
}
|
||||
"ec.color.colorama" => {
|
||||
let smooth = pr.get("outputCycle/interpolatePalette").is_none_or(|v| v.as_bool());
|
||||
// The posterized palette is discontinuous: phases on a step (common with 8 bpc
|
||||
// input) can round to the other side in the GPU's float maths, so it runs on the CPU.
|
||||
if !smooth {
|
||||
return None;
|
||||
}
|
||||
p.u[0] = [5, pr.e("inputPhase/mode"), smooth as u32, 0];
|
||||
p.f[0] = [pr.f("outputCycle/cycles").max(0.0) as f32, f("inputPhase/phaseShift") / 360.0, 1.0 - f("blend") / 100.0, 0.0];
|
||||
}
|
||||
"ec.color.channelmixer" => {
|
||||
let g = |k: &str| f(k) / 100.0;
|
||||
p.u[0] = [6, pr.b("monochrome") as u32, 0, 0];
|
||||
p.f[0] = [g("rr"), g("rg"), g("rb"), g("rc")];
|
||||
p.f[1] = [g("gr"), g("gg"), g("gb"), g("gc")];
|
||||
p.f[2] = [g("br"), g("bg"), g("bb"), g("bc")];
|
||||
}
|
||||
"ec.color.selectivecolor" => {
|
||||
let adj: Vec<f32> = SC_IDS.iter().flat_map(|ids| ids.map(|k| f(k) / 100.0)).collect();
|
||||
if adj.iter().all(|v| *v == 0.0) {
|
||||
return Some(b);
|
||||
}
|
||||
p.u[0] = [7, (pr.e("method") == 0) as u32, 0, 0];
|
||||
data = Some(e.data(&adj));
|
||||
}
|
||||
"ec.key.linearcolor" => {
|
||||
let view = pr.e("view");
|
||||
if view == 1 {
|
||||
return Some(b);
|
||||
}
|
||||
let k = pr.color("keyColor");
|
||||
let s = k[0] + k[1] + k[2];
|
||||
let kc = if s <= 1e-6 { [1.0 / 3.0, 1.0 / 3.0] } else { [k[0] / s, k[1] / s] };
|
||||
let (kh, _, _) = rgb_to_hsl(k[0], k[1], k[2]);
|
||||
p.u[0] = [8, pr.e("matchColors"), (pr.e("keyOperation") == 1) as u32, view];
|
||||
p.f[0] = [k[0], k[1], k[2], f("tolerance") / 100.0];
|
||||
p.f[1] = [f("softness") / 100.0, kc[0], kc[1], kh];
|
||||
}
|
||||
_ => return None,
|
||||
}
|
||||
let out = e.image(b.img.width, b.img.height);
|
||||
e.pixels("fxc_point", &p, &b.img, None, &out, data.as_ref());
|
||||
Some(GBuf { img: out, ..b })
|
||||
}
|
||||
|
||||
/// color3::tint_offset.
|
||||
fn tint_offset(c: [f32; 4]) -> [f32; 3] {
|
||||
let l = (c[0] + c[1] + c[2]) / 3.0;
|
||||
[c[0] - l, c[1] - l, c[2] - l]
|
||||
}
|
||||
|
||||
fn lumetri(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let pr = ctx.params;
|
||||
let f = |k: &str| (pr.f(k) / 100.0) as f32;
|
||||
let mut d: Vec<f32> = vec![
|
||||
f("basicCorrection/whiteBalance/temperature"),
|
||||
f("basicCorrection/whiteBalance/tint"),
|
||||
2f32.powf(pr.f("basicCorrection/tone/exposure") as f32),
|
||||
f("basicCorrection/tone/contrast"),
|
||||
f("basicCorrection/tone/highlights"),
|
||||
f("basicCorrection/tone/shadows"),
|
||||
f("basicCorrection/tone/whites"),
|
||||
f("basicCorrection/tone/blacks"),
|
||||
f("basicCorrection/saturation"),
|
||||
f("creative/lookIntensity"),
|
||||
f("creative/adjustments/fadedFilm"),
|
||||
f("creative/adjustments/vibrance"),
|
||||
f("creative/adjustments/creativeSaturation"),
|
||||
(0.5 + 0.25 * f("creative/adjustments/tintBalance")).clamp(0.05, 0.95),
|
||||
];
|
||||
d.extend(tint_offset(pr.color("creative/adjustments/shadowTint")));
|
||||
d.extend(tint_offset(pr.color("creative/adjustments/highlightTint")));
|
||||
for c in ["Master", "Red", "Green", "Blue"] {
|
||||
for part in ["Shadows", "Midtones", "Highlights"] {
|
||||
d.push(f(&format!("curves/curve{c}{part}")));
|
||||
}
|
||||
}
|
||||
for w in ["shadowsWheel", "midtonesWheel", "highlightsWheel"] {
|
||||
d.extend(tint_offset(pr.color(&format!("colorWheels/{w}"))));
|
||||
}
|
||||
let (lx, ly) = (b.offset[0], b.offset[1]);
|
||||
let (lw, lh) = (ctx.layer_size[0] * b.scale, ctx.layer_size[1] * b.scale);
|
||||
d.extend([
|
||||
pr.f("vignette/vignetteAmount") as f32,
|
||||
f("vignette/vignetteMidpoint") * 0.9,
|
||||
f("vignette/vignetteRoundness"),
|
||||
f("vignette/vignetteFeather"),
|
||||
(lx + lw * 0.5) as f32,
|
||||
(ly + lh * 0.5) as f32,
|
||||
lw as f32,
|
||||
lh as f32,
|
||||
(lw / lh.max(1e-6)) as f32,
|
||||
]);
|
||||
debug_assert_eq!(d.len(), 50);
|
||||
let buf = e.data(&d);
|
||||
let out = e.image(b.img.width, b.img.height);
|
||||
e.pixels("fxc_lumetri", &Params::default(), &b.img, None, &out, Some(&buf));
|
||||
let sharpen = f("creative/adjustments/sharpen");
|
||||
if sharpen == 0.0 {
|
||||
return Some(GBuf { img: out, ..b });
|
||||
}
|
||||
let luma = e.image(out.width, out.height);
|
||||
e.pixels("fxc_luma", &Params::default(), &out, None, &luma, None);
|
||||
let blurred = gaussian_blur(e, &luma, b.scale, b.scale, true);
|
||||
let mut p = Params::default();
|
||||
p.f[0][0] = sharpen * 2.0;
|
||||
let sharp = e.image(out.width, out.height);
|
||||
e.pixels("fxc_sharpen", &p, &out, Some(&blurred), &sharp, None);
|
||||
Some(GBuf { img: sharp, ..b })
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- planes
|
||||
|
||||
/// util::morph_plane with the same radius on both axes; `modes` = per-channel operation
|
||||
/// (0 erode, 1 dilate, 2 keep).
|
||||
fn morph(e: &mut Enc, img: &GpuImage, r: u32, modes: [u32; 4]) -> GpuImage {
|
||||
if r == 0 {
|
||||
return img.clone();
|
||||
}
|
||||
let mut cur = img.clone();
|
||||
for vertical in [0, 1] {
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [r, vertical, 0, 0];
|
||||
p.u[1] = modes;
|
||||
let out = e.image(cur.width, cur.height);
|
||||
e.pixels("fxc_morph", &p, &cur, None, &out, None);
|
||||
cur = out;
|
||||
}
|
||||
cur
|
||||
}
|
||||
|
||||
/// util::morph_frac (blend between the two nearest integer radii).
|
||||
fn morph_frac(e: &mut Enc, img: &GpuImage, radius: f64, modes: [u32; 4]) -> GpuImage {
|
||||
if radius <= 0.0 {
|
||||
return img.clone();
|
||||
}
|
||||
let r0 = radius.floor() as u32;
|
||||
let t = (radius - r0 as f64) as f32;
|
||||
let a = morph(e, img, r0, modes);
|
||||
if t < 1e-4 {
|
||||
return a;
|
||||
}
|
||||
let b = morph(e, img, r0 + 1, modes);
|
||||
let mut p = Params::default();
|
||||
p.f[0][0] = t;
|
||||
let out = e.image(img.width, img.height);
|
||||
e.pixels("fxc_lerp", &p, &a, Some(&b), &out, None);
|
||||
out
|
||||
}
|
||||
|
||||
const ERODE_X: [u32; 4] = [0, 2, 2, 2];
|
||||
const DILATE_X: [u32; 4] = [1, 2, 2, 2];
|
||||
|
||||
// ---------------------------------------------------------------- Key Light
|
||||
|
||||
/// keylight::primary.
|
||||
fn primary(s: [f32; 3]) -> [u32; 3] {
|
||||
let pi: usize = if s[1] >= s[0] && s[1] >= s[2] {
|
||||
1
|
||||
} else if s[2] >= s[0] {
|
||||
2
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let (a, b) = match pi {
|
||||
0 => (1, 2),
|
||||
1 => (0, 2),
|
||||
_ => (0, 1),
|
||||
};
|
||||
let ix = if s[a] >= s[b] { [pi, a, b] } else { [pi, b, a] };
|
||||
ix.map(|i| i as u32)
|
||||
}
|
||||
|
||||
fn neutralise(c: [f32; 3], bias: [f32; 3]) -> [f32; 3] {
|
||||
let l = luminance(bias[0], bias[1], bias[2]).max(1e-4);
|
||||
[0, 1, 2].map(|i| c[i] * l / bias[i].max(1e-4))
|
||||
}
|
||||
|
||||
fn key_light(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let pr = ctx.params;
|
||||
let view = pr.e("view");
|
||||
if view == 0 {
|
||||
return Some(b);
|
||||
}
|
||||
// Inside / Outside masks (polygon coverage) run on the CPU.
|
||||
let has_mask = |id: &str| pr.e(id).checked_sub(1).is_some_and(|i| ctx.env.masks.get(i as usize).is_some());
|
||||
if has_mask("insideMask/insideMask") || has_mask("outsideMask/outsideMask") {
|
||||
return None;
|
||||
}
|
||||
let sc = pr.color("screenColour");
|
||||
let screen = [sc[0], sc[1], sc[2]];
|
||||
let ix = primary(screen);
|
||||
let (pi, o1, o2) = (ix[0] as usize, ix[1] as usize, ix[2] as usize);
|
||||
let gain = pr.f("screenGain") as f32 / 100.0;
|
||||
let bal = pr.f("screenBalance") as f32 / 100.0;
|
||||
let dbias = pr.color("despillBias");
|
||||
let abias = if pr.b("lockBiasesTogether") { dbias } else { pr.color("alphaBias") };
|
||||
let (dbias, abias) = ([dbias[0], dbias[1], dbias[2]], [abias[0], abias[1], abias[2]]);
|
||||
let s_alpha = neutralise(screen, abias);
|
||||
let sd = (s_alpha[pi] - (bal * s_alpha[o1] + (1.0 - bal) * s_alpha[o2])).max(1e-3);
|
||||
let cb = pr.f("screenMatte/clipBlack") as f32 / 100.0;
|
||||
let cw = pr.f("screenMatte/clipWhite") as f32 / 100.0;
|
||||
let rollback = pr.f("screenMatte/clipRollback") * b.scale;
|
||||
let preblur = pr.f("screenPreblur") * b.scale;
|
||||
let sg = pr.f("screenMatte/screenShrinkGrow") * b.scale;
|
||||
let soft = pr.f("screenMatte/screenSoftness") * b.scale;
|
||||
let despot_b = pr.f("screenMatte/screenDespotBlack") * b.scale;
|
||||
let despot_w = pr.f("screenMatte/screenDespotWhite") * b.scale;
|
||||
// Hard Colour replaces wherever the screen matte exceeds the raw matte by any amount; after
|
||||
// Screen Softness's blur that compares float-rounded sums, so it runs on the CPU.
|
||||
if pr.e("screenMatte/replaceMethod") == 2 && soft > 0.0 {
|
||||
return None;
|
||||
}
|
||||
let (w, h) = (b.img.width, b.img.height);
|
||||
|
||||
let key_src = if preblur > 0.0 { gaussian_blur(e, &b.img, preblur * 0.5, preblur * 0.5, true) } else { b.img.clone() };
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [ix[0], ix[1], ix[2], 0];
|
||||
p.f[0] = [abias[0], abias[1], abias[2], sd];
|
||||
p.f[1] = [gain, bal, cb, cw];
|
||||
let raw = e.image(w, h);
|
||||
e.pixels("fxc_kl_raw", &p, &key_src, None, &raw, None);
|
||||
let mut m = raw.clone();
|
||||
if rollback > 0.0 {
|
||||
let sg_img = morph_frac(e, &m, rollback, [0, 1, 2, 2]);
|
||||
let out = e.image(w, h);
|
||||
e.pixels("fxc_kl_rollback", &Params::default(), &sg_img, Some(&raw), &out, None);
|
||||
m = out;
|
||||
}
|
||||
if despot_b > 0.0 {
|
||||
let d = morph_frac(e, &m, despot_b, DILATE_X);
|
||||
m = morph_frac(e, &d, despot_b, ERODE_X);
|
||||
}
|
||||
if despot_w > 0.0 {
|
||||
let d = morph_frac(e, &m, despot_w, ERODE_X);
|
||||
m = morph_frac(e, &d, despot_w, DILATE_X);
|
||||
}
|
||||
if sg < 0.0 {
|
||||
m = morph_frac(e, &m, -sg, ERODE_X);
|
||||
} else if sg > 0.0 {
|
||||
m = morph_frac(e, &m, sg, DILATE_X);
|
||||
}
|
||||
if soft > 0.0 {
|
||||
m = gaussian_blur(e, &m, soft * 0.5, soft * 0.5, true);
|
||||
}
|
||||
let mut planes = e.image(w, h);
|
||||
e.pixels("fxc_kl_pack", &Params::default(), &m, Some(&raw), &planes, None);
|
||||
let src_alpha_mode = pr.e("insideMask/sourceAlpha");
|
||||
if pr.b("edgeColourCorrection/enableEdgeColourCorrection") {
|
||||
let grow = pr.f("edgeColourCorrection/edgeGrow") * b.scale;
|
||||
let hard = (pr.f("edgeColourCorrection/edgeHardness") / 100.0) as f32;
|
||||
let esoft = pr.f("edgeColourCorrection/edgeSoftness") * b.scale;
|
||||
let mut p = Params::default();
|
||||
p.u[0][0] = src_alpha_mode;
|
||||
let mut band = e.image(w, h);
|
||||
e.pixels("fxc_kl_band", &p, &b.img, Some(&planes), &band, None);
|
||||
if grow > 0.0 {
|
||||
band = morph_frac(e, &band, grow, DILATE_X);
|
||||
}
|
||||
if esoft > 0.0 {
|
||||
band = gaussian_blur(e, &band, esoft * 0.5, esoft * 0.5, true);
|
||||
}
|
||||
let mut p = Params::default();
|
||||
p.f[0][0] = hard;
|
||||
let out = e.image(w, h);
|
||||
e.pixels("fxc_kl_edge", &p, &planes, Some(&band), &out, None);
|
||||
planes = out;
|
||||
}
|
||||
let pc = |k: &str| pr.f(k) as f32 / 100.0;
|
||||
let l0 = (luminance(dbias[0], dbias[1], dbias[2]) / dbias[pi].max(1e-4)).max(1e-4);
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [ix[0], ix[1], ix[2], view];
|
||||
p.u[1] = [
|
||||
pr.e("screenMatte/replaceMethod"),
|
||||
pr.b("foregroundColourCorrection/enableColourCorrection") as u32,
|
||||
pr.b("unpremultiplyResult") as u32,
|
||||
src_alpha_mode,
|
||||
];
|
||||
p.f[0] = [screen[0], screen[1], screen[2], bal];
|
||||
p.f[1] = [dbias[0], dbias[1], dbias[2], l0];
|
||||
p.f[2] = pr.color("screenMatte/replaceColour");
|
||||
p.f[3] =
|
||||
[pc("foregroundColourCorrection/saturation"), pc("foregroundColourCorrection/contrast") + 1.0, pc("foregroundColourCorrection/brightness") + 1.0, 0.0];
|
||||
p.f[4] = [pc("edgeColourCorrection/edgeSaturation"), pc("edgeColourCorrection/edgeContrast") + 1.0, pc("edgeColourCorrection/edgeBrightness") + 1.0, 0.0];
|
||||
p.f[5] = ["sourceCrops/cropLeft", "sourceCrops/cropRight", "sourceCrops/cropTop", "sourceCrops/cropBottom"].map(|k| (pr.f(k) / 100.0) as f32);
|
||||
p.f[6] = [b.offset[0] as f32, b.offset[1] as f32, b.scale as f32, 0.0];
|
||||
p.f[7] = [ctx.layer_size[0].max(1e-9) as f32, ctx.layer_size[1].max(1e-9) as f32, 0.0, 0.0];
|
||||
let out = e.image(w, h);
|
||||
e.pixels("fxc_kl_final", &p, &b.img, Some(&planes), &out, None);
|
||||
Some(GBuf { img: out, ..b })
|
||||
}
|
||||
|
|
@ -0,0 +1,413 @@
|
|||
//! GPU effects, distortion and stylize family (kernels in `shaders/fx_distort.wgsl`): the CPU effects' exact
|
||||
//! steps with the pixel loops as compute kernels.
|
||||
//!
|
||||
//! Parameter conversions, padding and anything with a per-row or per-column structure (map
|
||||
//! coordinates, tile bounds, Mesh Warp's triangle buckets) are computed here in f64 exactly as
|
||||
//! the CPU does and uploaded; the kernels do the per-pixel work.
|
||||
|
||||
use std::f64::consts::TAU;
|
||||
|
||||
use effectcraft_effects::EffectCtx;
|
||||
use effectcraft_effects::util::{Src, src_at};
|
||||
|
||||
use crate::context::{Enc, Params};
|
||||
use crate::effects::GBuf;
|
||||
|
||||
/// Compute entry points in `fx_distort.wgsl`.
|
||||
pub(crate) const KERNELS: &[&str] = &[
|
||||
"dst_twirl",
|
||||
"dst_bulge",
|
||||
"dst_wavewarp",
|
||||
"dst_ripple",
|
||||
"dst_cclens",
|
||||
"dst_turbulent",
|
||||
"dst_displace",
|
||||
"dst_mesh",
|
||||
"dst_mosaic_rows",
|
||||
"dst_mosaic_tiles",
|
||||
"dst_mosaic_out",
|
||||
"dst_mosaic_sharp",
|
||||
"dst_findedges",
|
||||
"dst_emboss",
|
||||
];
|
||||
|
||||
/// Effect ids implemented here.
|
||||
pub(crate) const IDS: &[&str] = &[
|
||||
"ec.distort.turbulentdisplace",
|
||||
"ec.distort.displacementmap",
|
||||
"ec.distort.wavewarp",
|
||||
"ec.distort.ripple",
|
||||
"ec.distort.twirl",
|
||||
"ec.distort.bulge",
|
||||
"ec.distort.cclens",
|
||||
"ec.distort.meshwarp",
|
||||
"ec.stylize.mosaic",
|
||||
"ec.stylize.findedges",
|
||||
"ec.stylize.emboss",
|
||||
];
|
||||
|
||||
/// 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.distort.turbulentdisplace" => turbulent(e, ctx, b),
|
||||
"ec.distort.displacementmap" => displacement_map(e, ctx, b),
|
||||
"ec.distort.wavewarp" => wave_warp(e, ctx, b),
|
||||
"ec.distort.ripple" => ripple(e, ctx, b),
|
||||
"ec.distort.twirl" => twirl(e, ctx, b),
|
||||
"ec.distort.bulge" => bulge(e, ctx, b),
|
||||
"ec.distort.cclens" => cc_lens(e, ctx, b),
|
||||
"ec.distort.meshwarp" => mesh_warp(e, ctx, b),
|
||||
"ec.stylize.mosaic" => mosaic(e, ctx, b),
|
||||
"ec.stylize.findedges" => find_edges(e, ctx, b),
|
||||
"ec.stylize.emboss" => emboss(e, ctx, b),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Run a same-size per-pixel kernel over the buffer.
|
||||
fn run(e: &mut Enc, entry: &str, p: &Params, mut b: GBuf, data: Option<&wgpu::Buffer>) -> Option<GBuf> {
|
||||
let out = e.image(b.img.width, b.img.height);
|
||||
e.pixels(entry, p, &b.img, None, &out, data);
|
||||
b.img = out;
|
||||
Some(b)
|
||||
}
|
||||
|
||||
/// Buf::layer_w / layer_h (distort.rs).
|
||||
fn layer_wh(ctx: &EffectCtx) -> (f64, f64) {
|
||||
(ctx.layer_size[0].max(1.0), ctx.layer_size[1].max(1.0))
|
||||
}
|
||||
|
||||
/// util::layer_rect.
|
||||
fn layer_rect(ctx: &EffectCtx, b: &GBuf) -> [f32; 4] {
|
||||
[b.offset[0] as f32, b.offset[1] as f32, (ctx.layer_size[0] * b.scale) as f32, (ctx.layer_size[1] * b.scale) as f32]
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- distort.rs
|
||||
|
||||
fn twirl(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let ang = ctx.params.f("angle").to_radians();
|
||||
let (lw, lh) = layer_wh(ctx);
|
||||
let r = (ctx.params.f("radius") / 100.0 * lw.max(lh) * b.scale).max(1.0);
|
||||
let c = b.to_px(ctx.params.v2("center"));
|
||||
let mut p = Params::default();
|
||||
p.f[0] = [c.0 as f32, c.1 as f32, r as f32, ang as f32];
|
||||
run(e, "dst_twirl", &p, b, None)
|
||||
}
|
||||
|
||||
fn bulge(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let rx = (ctx.params.f("hradius") * b.scale).max(1.0);
|
||||
let ry = (ctx.params.f("vradius") * b.scale).max(1.0);
|
||||
let c = b.to_px(ctx.params.v2("center"));
|
||||
let h = ctx.params.f("height");
|
||||
let taper = 1.0 + ctx.params.f("taperRadius").max(0.0) / 100.0 * 3.0;
|
||||
let pin = ctx.params.b("pinAllEdges");
|
||||
let (lw, lh) = layer_wh(ctx);
|
||||
let (lw, lh) = (lw * b.scale, lh * b.scale);
|
||||
let edge = (lw.min(lh) * 0.1).max(1.0);
|
||||
let mut p = Params::default();
|
||||
p.f[0] = [c.0 as f32, c.1 as f32, rx as f32, ry as f32];
|
||||
p.f[1] = [h as f32, taper as f32, if pin { 1.0 } else { 0.0 }, edge as f32];
|
||||
p.f[2] = [b.offset[0] as f32, b.offset[1] as f32, lw as f32, lh as f32];
|
||||
run(e, "dst_bulge", &p, b, None)
|
||||
}
|
||||
|
||||
fn wave_warp(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
|
||||
let h = ctx.params.f("height") * b.scale;
|
||||
let w = (ctx.params.f("width") * b.scale).max(1.0);
|
||||
let dir = ctx.params.f("direction").to_radians();
|
||||
let speed = ctx.params.f("speed");
|
||||
let phase = ctx.params.f("phase").to_radians() + ctx.time * speed * TAU;
|
||||
let (dx, dy) = (dir.sin(), -dir.cos());
|
||||
if !ctx.adjustment {
|
||||
b.pad(e, h.abs().ceil() as u32 + 1)?;
|
||||
}
|
||||
let (lw, lh) = layer_wh(ctx);
|
||||
let rect = [b.offset[0] as f32, b.offset[1] as f32, (lw * b.scale).max(1.0) as f32, (lh * b.scale).max(1.0) as f32];
|
||||
// The wave phase in cycles, (x·dx + y·dy) / width + phase / 2π, is separable: per column
|
||||
// and per row (fraction, whole cycles) in f64, so the kernel's sum keeps the fraction exact
|
||||
// to f32 (the circle shapes have vertical tangents where a phase error is amplified).
|
||||
let (bw, bh) = (b.img.width as usize, b.img.height as usize);
|
||||
let mut data = vec![0.0f32; 2 * (bw + bh)];
|
||||
let split = |v: f64| (v - v.floor(), v.floor());
|
||||
for x in 0..bw {
|
||||
let (f, i) = split((x as f64 + 0.5) * dx / w);
|
||||
data[x] = f as f32;
|
||||
data[bw + x] = i as f32;
|
||||
}
|
||||
for y in 0..bh {
|
||||
let (f, i) = split((y as f64 + 0.5) * dy / w + phase / TAU);
|
||||
data[2 * bw + y] = f as f32;
|
||||
data[2 * bw + bh + y] = i as f32;
|
||||
}
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [ctx.params.e("waveType"), ctx.params.e("pinning"), ctx.seed ^ 0x3a7e, bw as u32];
|
||||
p.f[0] = [dx as f32, dy as f32, h as f32, 0.0];
|
||||
p.f[2] = rect;
|
||||
let buf = e.data(&data);
|
||||
run(e, "dst_wavewarp", &p, b, Some(&buf))
|
||||
}
|
||||
|
||||
fn ripple(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let (lw, lh) = layer_wh(ctx);
|
||||
let r = (ctx.params.f("radius") / 100.0 * lw.max(lh) * 0.5 * b.scale).max(1.0);
|
||||
let asymmetric = ctx.params.e("conversion") == 0;
|
||||
let c = b.to_px(ctx.params.v2("center"));
|
||||
let w = (ctx.params.f("waveWidth") * b.scale).max(1.0);
|
||||
let h = ctx.params.f("waveHeight") * b.scale;
|
||||
let phase = ctx.params.f("phase").to_radians() - ctx.time * ctx.params.f("speed") * TAU;
|
||||
let mut p = Params::default();
|
||||
p.u[0][0] = asymmetric as u32;
|
||||
p.f[0] = [c.0 as f32, c.1 as f32, r as f32, w as f32];
|
||||
p.f[1] = [h as f32, phase.rem_euclid(TAU) as f32, 0.0, 0.0];
|
||||
run(e, "dst_ripple", &p, b, None)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- distort2.rs
|
||||
|
||||
fn cc_lens(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let c = b.to_px(ctx.params.v2("center"));
|
||||
let (lw, lh) = (ctx.layer_size[0] * b.scale, ctx.layer_size[1] * b.scale);
|
||||
let r = (ctx.params.f("size") / 100.0 * lw.max(lh) * 0.5).max(0.5);
|
||||
let k = 2f64.powf(ctx.params.f("convergence") / 50.0);
|
||||
let mut p = Params::default();
|
||||
p.f[0] = [c.0 as f32, c.1 as f32, r as f32, k as f32];
|
||||
run(e, "dst_cclens", &p, b, None)
|
||||
}
|
||||
|
||||
fn turbulent(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
|
||||
let amount = ctx.params.f("amount") * b.scale;
|
||||
if amount.abs() < 1e-9 {
|
||||
return Some(b);
|
||||
}
|
||||
let kind = ctx.params.e("displacement");
|
||||
let size = (ctx.params.f("size") * b.scale).max(1.0);
|
||||
if ctx.params.b("resizeLayer") && !ctx.adjustment {
|
||||
b.pad(e, amount.abs().ceil() as u32 + 1)?;
|
||||
}
|
||||
let off = b.to_px(ctx.params.v2("offset"));
|
||||
let oct = ctx.params.f("complexity").clamp(1.0, 10.0);
|
||||
let evo = ctx.params.f("evolution") / 360.0;
|
||||
let cycle = ctx.params.b("evolutionOptions/cycleEvolution").then(|| ctx.params.f("evolutionOptions/cycle").round().max(1.0));
|
||||
let falloff = if (3..=5).contains(&kind) { 0.3 } else { 0.5 };
|
||||
let rect = layer_rect(ctx, &b);
|
||||
let edge = ((ctx.layer_size[0] * b.scale).min(ctx.layer_size[1] * b.scale) * 0.1).max(1.0);
|
||||
let seed = (ctx.seed ^ 0x7d15).wrapping_add((ctx.params.f("evolutionOptions/randomSeed") as i64 as u32).wrapping_mul(0x9e37_79b9));
|
||||
let (z0, z1, t) = match cycle {
|
||||
Some(c) => {
|
||||
let z = evo.rem_euclid(c);
|
||||
(z as f32, (z - c) as f32, (z / c) as f32)
|
||||
}
|
||||
None => (evo as f32, 0.0, 0.0),
|
||||
};
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [kind, ctx.params.e("pinning"), seed, oct.ceil().max(1.0) as u32];
|
||||
p.u[1][0] = cycle.is_some() as u32;
|
||||
p.f[0] = [off.0 as f32, off.1 as f32, size as f32, amount as f32];
|
||||
p.f[1] = [z0, z1, t, (oct - oct.floor()) as f32];
|
||||
p.f[2] = rect;
|
||||
p.f[3] = [edge as f32, falloff, 0.0, 0.0];
|
||||
run(e, "dst_turbulent", &p, b, None)
|
||||
}
|
||||
|
||||
fn displacement_map(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
|
||||
let hs = src_at(ctx.params.e("useForHorizontal"));
|
||||
let vs = src_at(ctx.params.e("useForVertical"));
|
||||
let mh = ctx.params.f("maxHorizontal") * b.scale;
|
||||
let mv = ctx.params.f("maxVertical") * b.scale;
|
||||
let wrap = ctx.params.b("wrapPixelsAround");
|
||||
if ctx.params.b("expandOutput") && !wrap && !ctx.adjustment {
|
||||
b.pad(e, mh.abs().max(mv.abs()).ceil() as u32)?;
|
||||
}
|
||||
let layer = ctx.layer_param("displacementMapLayer", false);
|
||||
let behavior = ctx.params.e("displacementMapBehavior");
|
||||
let (w, h) = (b.img.width as usize, b.img.height as usize);
|
||||
let inv = 1.0 / b.scale.max(1e-9);
|
||||
let ls = ctx.layer_size;
|
||||
// Per column / row: the map layer's buffer coordinate (fit_layer / Tile Map) and whether
|
||||
// the pixel lies inside the centred map (Center Map).
|
||||
let mut data = vec![0.0f32; 2 * (w + h)];
|
||||
let mut mode = 0u32;
|
||||
let mut center = false;
|
||||
let map = match &layer {
|
||||
Some(o) => {
|
||||
if o.buf.img.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let os = o.size;
|
||||
let axis = |i: usize, k: usize| -> (f64, bool) {
|
||||
let q = (i as f64 + 0.5 - b.offset[k]) * inv;
|
||||
let map_q = match behavior {
|
||||
2 => (q + (os[k] - ls[k]) * 0.5).rem_euclid(os[k].max(1.0)),
|
||||
1 if ls[0] > 0.0 && ls[1] > 0.0 => q * (os[k] / ls[k]),
|
||||
1 => q,
|
||||
_ => q + (os[k] - ls[k]) * 0.5,
|
||||
};
|
||||
let c = q + (os[k] - ls[k]) * 0.5;
|
||||
(map_q * o.buf.scale + o.buf.offset[k], (0.0..os[k]).contains(&c))
|
||||
};
|
||||
for x in 0..w {
|
||||
let (m, inside) = axis(x, 0);
|
||||
data[x] = m as f32;
|
||||
data[w + h + x] = inside as u32 as f32;
|
||||
}
|
||||
for y in 0..h {
|
||||
let (m, inside) = axis(y, 1);
|
||||
data[w + y] = m as f32;
|
||||
data[2 * w + h + y] = inside as u32 as f32;
|
||||
}
|
||||
mode = if behavior == 2 { 2 } else { 1 };
|
||||
center = behavior == 0;
|
||||
Some(e.g.upload_image(&o.buf.img)?)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
let idx = |s: Src| effectcraft_effects::util::SRC_ORDER.iter().position(|x| *x == s).unwrap_or(10) as u32;
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [idx(hs), idx(vs), mode, wrap as u32 | (center as u32) << 1];
|
||||
p.u[1] = [w as u32, h as u32, 0, 0];
|
||||
p.f[0] = [mh as f32, mv as f32, 0.0, 0.0];
|
||||
let buf = e.data(&data);
|
||||
let out = e.image(b.img.width, b.img.height);
|
||||
e.pixels("dst_displace", &p, &b.img, map.as_ref(), &out, Some(&buf));
|
||||
b.img = out;
|
||||
Some(b)
|
||||
}
|
||||
|
||||
fn mesh_warp(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let rows = ctx.params.f("rows").round().clamp(1.0, 31.0) as usize;
|
||||
let cols = ctx.params.f("columns").round().clamp(1.0, 31.0) as usize;
|
||||
let q = ctx.params.f("quality").round().clamp(1.0, 10.0) as usize;
|
||||
let offs = effectcraft_effects::parse_mesh(ctx.params.s("mesh"));
|
||||
if offs.len() != (rows + 1) * (cols + 1) || offs.iter().all(|o| o.0 == 0.0 && o.1 == 0.0) {
|
||||
return Some(b);
|
||||
}
|
||||
let (lw, lh) = (ctx.layer_size[0], ctx.layer_size[1]);
|
||||
let vert = |i: usize, j: usize| {
|
||||
let o = offs[j * (cols + 1) + i];
|
||||
b.to_px([i as f64 / cols as f64 * lw + o.0, j as f64 / rows as f64 * lh + o.1])
|
||||
};
|
||||
let (nx, ny) = (cols * q, rows * q);
|
||||
let mut dest = Vec::with_capacity((nx + 1) * (ny + 1));
|
||||
let mut srcp = Vec::with_capacity((nx + 1) * (ny + 1));
|
||||
for gj in 0..=ny {
|
||||
for gi in 0..=nx {
|
||||
let (ci, cj) = ((gi / q).min(cols - 1), (gj / q).min(rows - 1));
|
||||
let (u, v) = ((gi - ci * q) as f64 / q as f64, (gj - cj * q) as f64 / q as f64);
|
||||
let (p00, p10, p01, p11) = (vert(ci, cj), vert(ci + 1, cj), vert(ci, cj + 1), vert(ci + 1, cj + 1));
|
||||
let x = (p00.0 * (1.0 - u) + p10.0 * u) * (1.0 - v) + (p01.0 * (1.0 - u) + p11.0 * u) * v;
|
||||
let y = (p00.1 * (1.0 - u) + p10.1 * u) * (1.0 - v) + (p01.1 * (1.0 - u) + p11.1 * u) * v;
|
||||
dest.push((x, y));
|
||||
srcp.push(b.to_px([gi as f64 / nx as f64 * lw, gj as f64 / ny as f64 * lh]));
|
||||
}
|
||||
}
|
||||
// distort2::grid_warp: triangles in order, bucketed per output row.
|
||||
let idx = |i: usize, j: usize| j * (nx + 1) + i;
|
||||
let hh = b.img.height as usize;
|
||||
let mut tri_data: Vec<f32> = Vec::with_capacity(nx * ny * 24);
|
||||
let mut buckets: Vec<Vec<u32>> = vec![Vec::new(); hh];
|
||||
let mut t = 0u32;
|
||||
for j in 0..ny {
|
||||
for i in 0..nx {
|
||||
let (a, b2, c, d) = (idx(i, j), idx(i + 1, j), idx(i + 1, j + 1), idx(i, j + 1));
|
||||
for tri in [[a, b2, c], [a, c, d]] {
|
||||
let [ia, ib, ic] = tri;
|
||||
let (pa, pb, pc) = (dest[ia], dest[ib], dest[ic]);
|
||||
let den: f64 = (pb.1 - pc.1) * (pa.0 - pc.0) + (pc.0 - pb.0) * (pa.1 - pc.1);
|
||||
// Degenerate triangles are skipped per pixel on the CPU; leaving them out of
|
||||
// the buckets is the same.
|
||||
if den.abs() < 1e-12 {
|
||||
continue;
|
||||
}
|
||||
let ys = [pa.1, pb.1, pc.1];
|
||||
let y0 = (ys[0].min(ys[1]).min(ys[2]) - 0.5).floor().max(0.0) as usize;
|
||||
let y1 = (ys[0].max(ys[1]).max(ys[2]) + 0.5).ceil().max(0.0) as usize;
|
||||
for bucket in buckets.iter_mut().take(y1.min(hh)).skip(y0) {
|
||||
bucket.push(t);
|
||||
}
|
||||
let (sa, sb, sc) = (srcp[ia], srcp[ib], srcp[ic]);
|
||||
tri_data.extend(
|
||||
[(pb.1 - pc.1) / den, (pc.0 - pb.0) / den, (pc.1 - pa.1) / den, (pa.0 - pc.0) / den, pc.0, pc.1, sa.0, sa.1, sb.0, sb.1, sc.0, sc.1]
|
||||
.map(|v| v as f32),
|
||||
);
|
||||
t += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
let row_starts = tri_data.len();
|
||||
let list_start = row_starts + hh + 1;
|
||||
let total: usize = buckets.iter().map(Vec::len).sum();
|
||||
if list_start + total >= 1 << 24 {
|
||||
// Indices past 2^24 are not exact in the f32 data buffer.
|
||||
return None;
|
||||
}
|
||||
let mut data = tri_data;
|
||||
data.reserve(hh + 1 + total);
|
||||
let mut n = 0usize;
|
||||
for bucket in &buckets {
|
||||
data.push(n as f32);
|
||||
n += bucket.len();
|
||||
}
|
||||
data.push(n as f32);
|
||||
for bucket in &buckets {
|
||||
data.extend(bucket.iter().map(|&t| t as f32));
|
||||
}
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [row_starts as u32, list_start as u32, 0, 0];
|
||||
let buf = e.data(&data);
|
||||
run(e, "dst_mesh", &p, b, Some(&buf))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- misc.rs (Stylize)
|
||||
|
||||
fn mosaic(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
|
||||
let hb = ctx.params.f("horizontal").round().max(1.0) as usize;
|
||||
let vb = ctx.params.f("vertical").round().max(1.0) as usize;
|
||||
let (w, h) = (b.img.width as usize, b.img.height as usize);
|
||||
if w == 0 || h == 0 {
|
||||
return Some(b);
|
||||
}
|
||||
let cw = (w as f64 / hb as f64).max(1.0);
|
||||
let ch = (h as f64 / vb as f64).max(1.0);
|
||||
let mut p = Params::default();
|
||||
if ctx.params.b("sharpColors") {
|
||||
let centre = |i: usize, c: f64| (((i as f64 / c).floor() + 0.5) * c) as i64 as f32;
|
||||
let data: Vec<f32> = (0..w).map(|x| centre(x, cw)).chain((0..h).map(|y| centre(y, ch))).collect();
|
||||
p.u[0][0] = w as u32;
|
||||
let buf = e.data(&data);
|
||||
return run(e, "dst_mosaic_sharp", &p, b, Some(&buf));
|
||||
}
|
||||
let (nx, ny) = ((w as f64 / cw).ceil() as usize, (h as f64 / ch).ceil() as usize);
|
||||
let tx: Vec<usize> = (0..w).map(|x| ((x as f64 / cw) as usize).min(nx - 1)).collect();
|
||||
let ty: Vec<usize> = (0..h).map(|y| ((y as f64 / ch) as usize).min(ny - 1)).collect();
|
||||
// Tile bounds: tile i covers the columns with tx == i (tx is non-decreasing).
|
||||
let bounds = |t: &[usize], n: usize| -> Vec<f32> { (0..=n).map(|i| t.partition_point(|&v| v < i) as f32).collect() };
|
||||
let mut data: Vec<f32> = tx.iter().chain(&ty).map(|&v| v as f32).collect();
|
||||
data.extend(bounds(&tx, nx));
|
||||
data.extend(bounds(&ty, ny));
|
||||
p.u[0] = [w as u32, h as u32, nx as u32, ny as u32];
|
||||
let buf = e.data(&data);
|
||||
let rows = e.image(nx as u32, h as u32);
|
||||
e.pixels("dst_mosaic_rows", &p, &b.img, None, &rows, Some(&buf));
|
||||
let tiles = e.image(nx as u32, ny as u32);
|
||||
e.pixels("dst_mosaic_tiles", &p, &rows, None, &tiles, Some(&buf));
|
||||
let out = e.image(b.img.width, b.img.height);
|
||||
e.pixels("dst_mosaic_out", &p, &tiles, None, &out, Some(&buf));
|
||||
b.img = out;
|
||||
Some(b)
|
||||
}
|
||||
|
||||
fn find_edges(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let mut p = Params::default();
|
||||
p.u[0][0] = ctx.params.b("invert") as u32;
|
||||
p.f[0][0] = ctx.params.f("blend") as f32 / 100.0;
|
||||
run(e, "dst_findedges", &p, b, None)
|
||||
}
|
||||
|
||||
fn emboss(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let ang = ctx.params.f("direction").to_radians();
|
||||
let relief = ctx.params.f("relief") * b.scale;
|
||||
let (dx, dy) = (ang.cos() * relief, -ang.sin() * relief);
|
||||
let mut p = Params::default();
|
||||
p.f[0] = [dx as f32, dy as f32, ctx.params.f("contrast") as f32 / 100.0, ctx.params.f("blend") as f32 / 100.0];
|
||||
run(e, "dst_emboss", &p, b, None)
|
||||
}
|
||||
|
|
@ -0,0 +1,404 @@
|
|||
//! GPU effects, blur, transition and generate family (kernels in `shaders/fx_generate.wgsl`): the CPU effects' exact
|
||||
//! steps with the pixel loops as compute kernels.
|
||||
//!
|
||||
//! Every effect here covers all of its parameters. Where the CPU works in f64 around a hard
|
||||
//! edge (the wipes), the per-column and per-row terms are computed in f64 here and uploaded as
|
||||
//! (hi, lo) f32 pairs so the GPU puts the edge where the CPU does.
|
||||
|
||||
use effectcraft_color::BlendMode;
|
||||
use effectcraft_effects::{EffectCtx, GrainLook};
|
||||
|
||||
use crate::context::{Enc, Params};
|
||||
use crate::effects::{GBuf, gaussian_blur};
|
||||
|
||||
/// Compute entry points in `fx_generate.wgsl`.
|
||||
pub(crate) const KERNELS: &[&str] = &[
|
||||
"gen_radial_blur",
|
||||
"gen_ccradialfast",
|
||||
"gen_lens_pre",
|
||||
"gen_row_prefix",
|
||||
"gen_lens_blur",
|
||||
"gen_linear_wipe",
|
||||
"gen_venetian",
|
||||
"gen_radial_wipe",
|
||||
"gen_gradient_wipe",
|
||||
"gen_cell_pattern",
|
||||
"gen_checker",
|
||||
"gen_grid",
|
||||
"gen_fourcolor",
|
||||
"gen_noise",
|
||||
"gen_grain_planes",
|
||||
"gen_grain_apply",
|
||||
];
|
||||
|
||||
/// Effect ids implemented here.
|
||||
pub(crate) const IDS: &[&str] = &[
|
||||
"ec.blur.radial",
|
||||
"ec.blur.cameralens",
|
||||
"ec.blur.ccradialfast",
|
||||
"ec.transition.venetian",
|
||||
"ec.transition.linearwipe",
|
||||
"ec.transition.radialwipe",
|
||||
"ec.transition.gradientwipe",
|
||||
"ec.generate.cellpattern",
|
||||
"ec.generate.checkerboard",
|
||||
"ec.generate.grid",
|
||||
"ec.generate.fourcolor",
|
||||
"ec.noise.noise",
|
||||
"ec.noise.addgrain",
|
||||
];
|
||||
|
||||
/// Run effect `id` (one of [`IDS`]); `None` = this parameter combination runs on the CPU.
|
||||
pub(crate) fn apply(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
match id {
|
||||
"ec.blur.radial" => radial_blur(e, ctx, b),
|
||||
"ec.blur.cameralens" => camera_lens(e, ctx, b),
|
||||
"ec.blur.ccradialfast" => radial_fast(e, ctx, b),
|
||||
"ec.transition.venetian" => venetian(e, ctx, b),
|
||||
"ec.transition.linearwipe" => linear_wipe(e, ctx, b),
|
||||
"ec.transition.radialwipe" => radial_wipe(e, ctx, b),
|
||||
"ec.transition.gradientwipe" => gradient_wipe(e, ctx, b),
|
||||
"ec.generate.cellpattern" => cell_pattern(e, ctx, b),
|
||||
"ec.generate.checkerboard" => checkerboard(e, ctx, b),
|
||||
"ec.generate.grid" => grid(e, ctx, b),
|
||||
"ec.generate.fourcolor" => four_color(e, ctx, b),
|
||||
"ec.noise.noise" => noise(e, ctx, b),
|
||||
"ec.noise.addgrain" => add_grain(e, ctx, b),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Run a per-pixel kernel over the buffer.
|
||||
fn run(e: &mut Enc, entry: &str, p: &Params, b: GBuf, aux: Option<&crate::context::GpuImage>, data: Option<&[f32]>) -> Option<GBuf> {
|
||||
let buf = data.map(|d| e.data(d));
|
||||
let out = e.image(b.img.width, b.img.height);
|
||||
e.pixels(entry, p, &b.img, aux, &out, buf.as_ref());
|
||||
Some(GBuf { img: out, ..b })
|
||||
}
|
||||
|
||||
/// `v` as an f32 (hi, lo) pair.
|
||||
fn hl(v: f64) -> [f32; 2] {
|
||||
let h = v as f32;
|
||||
[h, (v - h as f64) as f32]
|
||||
}
|
||||
|
||||
/// Generate blending mode index → `BlendMode::ALL` index (255 = None: replace).
|
||||
fn gen_mode(i: u32) -> u32 {
|
||||
match effectcraft_effects::gen_mode(i) {
|
||||
None => 255,
|
||||
Some(m) => BlendMode::ALL.iter().position(|&x| x == m).unwrap_or(0) as u32,
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- blurs
|
||||
|
||||
fn radial_blur(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let amt = ctx.params.f("amount");
|
||||
let zoom = ctx.params.e("type") == 1;
|
||||
let c = b.to_px(ctx.params.v2("center"));
|
||||
let amount = if zoom { amt / 100.0 } else { amt };
|
||||
if amount.abs() < 1e-6 {
|
||||
return Some(b);
|
||||
}
|
||||
let n = 32usize;
|
||||
let mut data = Vec::with_capacity(2 * n);
|
||||
for i in 0..n {
|
||||
let t = i as f64 / (n - 1) as f64 - 0.5;
|
||||
if zoom {
|
||||
data.push((1.0 + t * amount) as f32);
|
||||
} else {
|
||||
let (s, c) = (t * amount).to_radians().sin_cos();
|
||||
data.extend([s as f32, c as f32]);
|
||||
}
|
||||
}
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [zoom as u32, n as u32, 0, 0];
|
||||
p.f[0] = [c.0 as f32, c.1 as f32, 0.0, 0.0];
|
||||
run(e, "gen_radial_blur", &p, b, None, Some(&data))
|
||||
}
|
||||
|
||||
fn radial_fast(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let s = (ctx.params.f("amount") / 100.0).clamp(0.0, 1.0) * 0.5;
|
||||
if s <= 0.0 {
|
||||
return Some(b);
|
||||
}
|
||||
let c = b.to_px(ctx.params.v2("center"));
|
||||
let mut p = Params::default();
|
||||
p.u[0][0] = ctx.params.e("zoom");
|
||||
p.f[0] = [c.0 as f32, c.1 as f32, s as f32, 0.0];
|
||||
run(e, "gen_ccradialfast", &p, b, None, None)
|
||||
}
|
||||
|
||||
fn camera_lens(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
|
||||
let r = ctx.params.f("blurRadius").max(0.0) * b.scale;
|
||||
if r < 0.5 {
|
||||
return Some(b);
|
||||
}
|
||||
let (spans, reach) = effectcraft_effects::camera_lens_spans(ctx, r);
|
||||
let repeat = ctx.params.b("repeatEdge") || ctx.adjustment;
|
||||
if !repeat {
|
||||
b.pad(e, reach.ceil() as u32 + 1)?;
|
||||
}
|
||||
let linear = ctx.params.b("useLinear");
|
||||
let thr = ctx.params.f("highlight/specularThreshold") as f32 / 255.0;
|
||||
let boost = 1.0 + ctx.params.f("highlight/specularBrightness").max(0.0) as f32 / 100.0 * 4.0;
|
||||
let sat = (ctx.params.f("highlight/highlightSaturation") / 100.0).clamp(0.0, 1.0) as f32;
|
||||
let (w, h) = (b.img.width, b.img.height);
|
||||
let mut src = b.img.clone();
|
||||
if linear || boost > 1.0 {
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [linear as u32, (boost > 1.0) as u32, 0, 0];
|
||||
p.f[0] = [thr, boost, sat, 0.0];
|
||||
let out = e.image(w, h);
|
||||
e.pixels("gen_lens_pre", &p, &src, None, &out, None);
|
||||
src = out;
|
||||
}
|
||||
let count: i64 = spans.iter().map(|(_, l, r)| r - l + 1).sum();
|
||||
let norm = 1.0 / count as f64;
|
||||
// Large irises sum each span from per-row prefix sums; small ones add the pixels directly.
|
||||
let prefix = count > 256;
|
||||
let pre = if prefix {
|
||||
if !e.g.fits(w + 1, h) {
|
||||
return None;
|
||||
}
|
||||
let pre = e.image(w + 1, h);
|
||||
e.dispatch("gen_row_prefix", &Params::default(), &src, None, &pre, None, (h.div_ceil(64), 1));
|
||||
Some(pre)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let data: Vec<f32> = spans.iter().flat_map(|&(dy, l, r)| [dy as f32, l as f32, r as f32]).collect();
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [spans.len() as u32, repeat as u32, linear as u32, prefix as u32];
|
||||
p.f[0][0] = norm as f32;
|
||||
let buf = e.data(&data);
|
||||
let out = e.image(w, h);
|
||||
e.pixels("gen_lens_blur", &p, &src, pre.as_ref(), &out, Some(&buf));
|
||||
b.img = out;
|
||||
Some(b)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- transitions
|
||||
|
||||
fn linear_wipe(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let done = ctx.params.f("completion") / 100.0;
|
||||
let ang = ctx.params.f("angle").to_radians();
|
||||
let feather = (ctx.params.f("feather") * b.scale).max(0.001);
|
||||
let (w, h) = (b.img.width as f64, b.img.height as f64);
|
||||
let (dx, dy) = (ang.sin(), -ang.cos());
|
||||
let corners = [(0.0, 0.0), (w, 0.0), (0.0, h), (w, h)];
|
||||
let proj: Vec<f64> = corners.iter().map(|(x, y)| x * dx + y * dy).collect();
|
||||
let (lo, hi) = (proj.iter().cloned().fold(f64::INFINITY, f64::min), proj.iter().cloned().fold(f64::NEG_INFINITY, f64::max));
|
||||
let edge = lo - feather + (hi - lo + 2.0 * feather) * done;
|
||||
let mut data = Vec::with_capacity(2 * (b.img.width + b.img.height) as usize);
|
||||
for x in 0..b.img.width {
|
||||
data.extend(hl((x as f64 + 0.5) * dx - edge));
|
||||
}
|
||||
for y in 0..b.img.height {
|
||||
data.extend(hl((y as f64 + 0.5) * dy));
|
||||
}
|
||||
let mut p = Params::default();
|
||||
p.u[0][0] = 2 * b.img.width;
|
||||
p.f[0][0] = feather as f32;
|
||||
run(e, "gen_linear_wipe", &p, b, None, Some(&data))
|
||||
}
|
||||
|
||||
fn venetian(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let done = ctx.params.f("completion") / 100.0;
|
||||
let ang = ctx.params.f("direction").to_radians();
|
||||
let width = (ctx.params.f("width") * b.scale).max(1.0);
|
||||
let feather = (ctx.params.f("feather") * b.scale).max(0.001);
|
||||
let (dx, dy) = (ang.cos(), ang.sin());
|
||||
let mut data = Vec::with_capacity((6 * b.img.width + 2 * b.img.height) as usize);
|
||||
for x in 0..b.img.width {
|
||||
let cx = ((x as f64 + 0.5) * dx).rem_euclid(width);
|
||||
data.extend(hl(cx - done * width));
|
||||
data.extend(hl(cx - width - done * width));
|
||||
data.extend(hl(cx - width));
|
||||
}
|
||||
for y in 0..b.img.height {
|
||||
data.extend(hl(((y as f64 + 0.5) * dy).rem_euclid(width)));
|
||||
}
|
||||
let mut p = Params::default();
|
||||
p.u[0][0] = 6 * b.img.width;
|
||||
p.f[0][0] = feather as f32;
|
||||
run(e, "gen_venetian", &p, b, None, Some(&data))
|
||||
}
|
||||
|
||||
fn radial_wipe(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let done = ctx.params.f("completion") / 100.0;
|
||||
let start = ctx.params.f("startAngle");
|
||||
let c = b.to_px(ctx.params.v2("center"));
|
||||
let dir = ctx.params.e("wipe");
|
||||
let feather = ctx.params.f("feather").max(0.01);
|
||||
let edge = if dir == 2 { done * 180.0 } else { done * 360.0 };
|
||||
// Rotations: the start angle, and the start angle ± the edge (where the wipe's edge lies).
|
||||
let rots = [start, start + edge, start - edge].map(|r| r.to_radians().sin_cos());
|
||||
let mut data = Vec::with_capacity(9 * (b.img.width + b.img.height) as usize);
|
||||
for x in 0..b.img.width {
|
||||
let ux = x as f64 + 0.5 - c.0;
|
||||
for (s, co) in rots {
|
||||
data.extend(hl(ux * co));
|
||||
data.push((ux * s) as f32);
|
||||
}
|
||||
}
|
||||
for y in 0..b.img.height {
|
||||
let uy = -(y as f64 + 0.5 - c.1);
|
||||
for (s, co) in rots {
|
||||
data.extend(hl(uy * s));
|
||||
data.push((uy * co) as f32);
|
||||
}
|
||||
}
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [9 * b.img.width, dir.min(2), 0, 0];
|
||||
p.f[0] = [edge as f32, feather as f32, 0.0, 0.0];
|
||||
run(e, "gen_radial_wipe", &p, b, None, Some(&data))
|
||||
}
|
||||
|
||||
fn gradient_wipe(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let done = (ctx.params.f("completion") / 100.0).clamp(0.0, 1.0) as f32;
|
||||
if done <= 0.0 {
|
||||
return Some(b);
|
||||
}
|
||||
let soft = (ctx.params.f("softness") / 100.0) as f32;
|
||||
// The gradient layer is fetched and placed on the CPU (as the CPU effect does).
|
||||
let grad = match ctx.layer_param("gradientLayer", false) {
|
||||
Some(o) => {
|
||||
// place_layer reads only the buffer's size, offset and scale.
|
||||
let img = effectcraft_raster::Image { width: b.img.width, height: b.img.height, data: vec![] };
|
||||
let cpu = effectcraft_effects::Buf { img, offset: b.offset, scale: b.scale };
|
||||
let img = effectcraft_effects::place_layer(ctx, &cpu, &o, ctx.params.e("gradientPlacement"));
|
||||
Some(e.g.upload_image(&img)?)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
let mut p = Params::default();
|
||||
p.u[0][0] = ctx.params.b("invert") as u32;
|
||||
p.f[0] = [done, soft, 0.0, 0.0];
|
||||
let out = e.image(b.img.width, b.img.height);
|
||||
e.pixels("gen_gradient_wipe", &p, &b.img, Some(grad.as_ref().unwrap_or(&b.img)), &out, None);
|
||||
Some(GBuf { img: out, ..b })
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- generate
|
||||
|
||||
fn cell_pattern(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let pr = ctx.params;
|
||||
let size = (pr.f("size") * b.scale).max(1.0);
|
||||
let off = b.to_px(pr.v2("offset"));
|
||||
let mut evo = pr.f("evolution") / 360.0;
|
||||
if pr.b("evolutionOptions/cycleEvolution") {
|
||||
evo = evo.rem_euclid(pr.f("evolutionOptions/cycle").round().max(1.0));
|
||||
}
|
||||
let seed = (pr.f("evolutionOptions/randomSeed") as i64 as u32).wrapping_mul(7919) ^ 0xce11;
|
||||
let tile = pr.b("tilingOptions/enableTiling");
|
||||
let nx = pr.f("tilingOptions/cellsHorizontal").round().max(1.0) as i64;
|
||||
let ny = pr.f("tilingOptions/cellsVertical").round().max(1.0) as i64;
|
||||
if nx > i32::MAX as i64 || ny > i32::MAX as i64 {
|
||||
return None;
|
||||
}
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [effectcraft_effects::cell_pattern_kind(pr.e("cellPattern")), pr.e("overflow"), pr.b("invert") as u32, seed];
|
||||
p.u[1] = [tile as u32, nx as u32, ny as u32, 0];
|
||||
// cos / sin of 2π·(h + evo) only see evo modulo 1.
|
||||
p.f[0] = [pr.f("contrast") as f32 / 100.0, pr.f("disperse").clamp(0.0, 1.5) as f32, size as f32, evo.rem_euclid(1.0) as f32];
|
||||
p.f[1] = [(0.5 - off.0) as f32, (0.5 - off.1) as f32, 0.0, 0.0];
|
||||
run(e, "gen_cell_pattern", &p, b, None, None)
|
||||
}
|
||||
|
||||
/// generate::cell_size.
|
||||
fn cell_size(ctx: &EffectCtx, b: &GBuf, anchor: (f64, f64)) -> (f64, f64) {
|
||||
let (w, h) = match ctx.params.e("sizeFrom") {
|
||||
0 => {
|
||||
let c = b.to_px(ctx.params.v2("corner"));
|
||||
((c.0 - anchor.0).abs(), (c.1 - anchor.1).abs())
|
||||
}
|
||||
1 => (ctx.params.f("width") * b.scale, ctx.params.f("width") * b.scale),
|
||||
_ => (ctx.params.f("width") * b.scale, ctx.params.f("height") * b.scale),
|
||||
};
|
||||
(w.max(1.0), h.max(1.0))
|
||||
}
|
||||
|
||||
fn checkerboard(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let anchor = b.to_px(ctx.params.v2("anchor"));
|
||||
let (w, h) = cell_size(ctx, &b, anchor);
|
||||
let fw = (ctx.params.f("feather/featherWidth") * b.scale).max(1.0);
|
||||
let fh = (ctx.params.f("feather/featherHeight") * b.scale).max(1.0);
|
||||
let mut p = Params::default();
|
||||
p.u[0][0] = gen_mode(ctx.params.e("blendingMode"));
|
||||
p.f[0] = [(0.5 - anchor.0) as f32, (0.5 - anchor.1) as f32, w as f32, h as f32];
|
||||
p.f[1] = [fw as f32, fh as f32, ctx.params.f("opacity") as f32 / 100.0, 0.0];
|
||||
p.f[2] = ctx.params.color("color");
|
||||
run(e, "gen_checker", &p, b, None, None)
|
||||
}
|
||||
|
||||
fn grid(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let anchor = b.to_px(ctx.params.v2("anchor"));
|
||||
let (w, h) = cell_size(ctx, &b, anchor);
|
||||
let border = (ctx.params.f("border") * b.scale).max(0.0);
|
||||
let fw = (ctx.params.f("feather/featherWidth") * b.scale).max(1.0);
|
||||
let fh = (ctx.params.f("feather/featherHeight") * b.scale).max(1.0);
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [gen_mode(ctx.params.e("blendingMode")), ctx.params.b("invertGrid") as u32, 0, 0];
|
||||
p.f[0] = [(0.5 - anchor.0) as f32, (0.5 - anchor.1) as f32, w as f32, h as f32];
|
||||
p.f[1] = [fw as f32, fh as f32, ctx.params.f("opacity") as f32 / 100.0, border as f32];
|
||||
p.f[2] = ctx.params.color("color");
|
||||
run(e, "gen_grid", &p, b, None, None)
|
||||
}
|
||||
|
||||
fn four_color(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let mut p = Params::default();
|
||||
for i in 0..4 {
|
||||
let q = b.to_px(ctx.params.v2(&format!("positionsColors/point{}", i + 1)));
|
||||
p.f[i] = [q.0 as f32, q.1 as f32, 0.0, 0.0];
|
||||
p.f[4 + i] = ctx.params.color(&format!("positionsColors/color{}", i + 1));
|
||||
}
|
||||
let blend = ctx.params.f("blend").max(1.0);
|
||||
let jitter = (ctx.params.f("jitter") / 100.0).clamp(0.0, 1.0) as f32;
|
||||
p.u[0] = [gen_mode(ctx.params.e("blendingMode")), ctx.seed, 0, 0];
|
||||
p.f[8] = [(blend * 100.0) as f32, jitter, ctx.params.f("opacity") as f32 / 100.0, 0.0];
|
||||
run(e, "gen_fourcolor", &p, b, None, None)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- noise
|
||||
|
||||
fn noise(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let amt = ctx.params.f("amount") as f32 / 100.0;
|
||||
let clip = ctx.params.get("clip").is_none_or(|v| v.as_bool());
|
||||
let frame_seed = (ctx.time * 1000.0) as u32 ^ ctx.seed;
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [frame_seed, ctx.params.b("color") as u32, clip as u32, b.img.width];
|
||||
p.f[0][0] = amt;
|
||||
run(e, "gen_noise", &p, b, None, None)
|
||||
}
|
||||
|
||||
fn add_grain(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let look = GrainLook::from_params(ctx);
|
||||
if look.intensity <= 0.0 {
|
||||
return Some(b);
|
||||
}
|
||||
let (w, h) = (b.img.width, b.img.height);
|
||||
// GrainLook::planes.
|
||||
let size = look.size * b.scale as f32;
|
||||
let s = |k: usize| (size * if look.mono { 1.0 } else { look.channel_size[k].max(0.05) }).max(0.05);
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [look.mono as u32, look.seed, 101, 0];
|
||||
p.f[0] = [s(0) * look.aspect, s(1) * look.aspect, s(2) * look.aspect, look.frame];
|
||||
p.f[1] = [s(0), s(1), s(2), 0.0];
|
||||
let mut planes = e.image(w, h);
|
||||
e.pixels("gen_grain_planes", &p, &b.img, None, &planes, None);
|
||||
let soft = look.softness * b.scale;
|
||||
if soft > 0.05 {
|
||||
planes = gaussian_blur(e, &planes, soft * look.aspect as f64, soft, true);
|
||||
}
|
||||
// GrainLook::color's tint factors.
|
||||
let l = effectcraft_color::luminance(look.tint[0], look.tint[1], look.tint[2]).max(1e-3);
|
||||
let tint = [0, 1, 2].map(|k| (1.0 - look.tint_amount) + look.tint_amount * look.tint[k] / l);
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [look.mono as u32, look.mode, (look.tint_amount > 0.0) as u32, 0];
|
||||
p.f[0] = [look.intensity * 0.25, look.saturation, look.midpoint, 0.0];
|
||||
p.f[1] = [tint[0], tint[1], tint[2], 0.0];
|
||||
p.f[2] = [look.channel_intensity[0], look.channel_intensity[1], look.channel_intensity[2], 0.0];
|
||||
p.f[3] = [look.tones[0], look.tones[1], look.tones[2], 0.0];
|
||||
run(e, "gen_grain_apply", &p, b, Some(&planes), None)
|
||||
}
|
||||
|
|
@ -6,8 +6,10 @@
|
|||
//! layer buffers (once per buffer), applies the layer transforms with the CPU's sampling
|
||||
//! (nearest / bilinear / bicubic, minification pre-filter), motion-blur sub-samples, track
|
||||
//! mattes, Preserve Transparency, layer styles' passes, all 38 blend modes and the 8/16 bpc
|
||||
//! clamping and quantisation, and converts colour spaces. 3D runs, adjustment layers and
|
||||
//! wireframes run on the CPU between GPU steps (read back, draw, upload).
|
||||
//! clamping and quantisation, and converts colour spaces. Classic 3D runs composite here too
|
||||
//! (`classic3d`: per-pixel fragment sort, lights, ray-cast shadows), and adjustment layers run
|
||||
//! their effect stacks on the GPU-resident comp. Advanced 3D compositing and wireframes run on
|
||||
//! the CPU between GPU steps (read back, draw, upload).
|
||||
//!
|
||||
//! Advanced 3D comps rasterise on a render pipeline (`advanced3d.wgsl`: depth buffer, PBR,
|
||||
//! image-based light, shadow maps), see [`Accelerator::raster_3d`].
|
||||
|
|
@ -16,15 +18,24 @@
|
|||
//! effect's exact steps (padding, box-blur radii, parameter conversions); chains of them are
|
||||
//! uploaded and read back once.
|
||||
//!
|
||||
//! GPU particles (`particles`): the stepped particle effects hand their simulation to
|
||||
//! [`effectcraft_effects::psim::ParticleSim`], implemented here with one invocation per particle
|
||||
//! and GPU-resident checkpoints.
|
||||
//!
|
||||
//! Plug a [`Gpu`] into [`Renderer::accel`] (it implements [`Accelerator`]); renders then use it
|
||||
//! when [`RenderOpts::backend`](effectcraft_render::RenderOpts) asks for it. The viewer can
|
||||
//! skip readback entirely with [`Gpu::render_display`], which leaves an RGBA8 texture for
|
||||
//! egui-wgpu to draw.
|
||||
|
||||
mod adv3d;
|
||||
mod classic3d;
|
||||
mod context;
|
||||
mod effects;
|
||||
mod fx_color;
|
||||
mod fx_distort;
|
||||
mod fx_generate;
|
||||
mod ops;
|
||||
mod particles;
|
||||
mod walk;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
|
@ -135,9 +146,31 @@ impl Accelerator for Gpu {
|
|||
fn raster_3d(&self, scene: &effectcraft_render::three_d::adv::Scene) -> Option<effectcraft_render::three_d::adv::Target> {
|
||||
adv3d::render(&self.ctx, scene)
|
||||
}
|
||||
|
||||
fn particles(&self) -> Option<&dyn effectcraft_effects::psim::ParticleSim> {
|
||||
self.ctx.can_readback().then_some(self as &dyn effectcraft_effects::psim::ParticleSim)
|
||||
}
|
||||
}
|
||||
|
||||
impl effectcraft_effects::psim::ParticleSim for Gpu {
|
||||
fn simulate(&self, req: &effectcraft_effects::psim::SimRequest) -> Option<Vec<effectcraft_effects::psim::SimParticle>> {
|
||||
particles::simulate(&self.ctx, req)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
#[cfg(test)]
|
||||
mod tests_3d;
|
||||
#[cfg(test)]
|
||||
mod tests_adjust;
|
||||
#[cfg(test)]
|
||||
mod tests_adv3d;
|
||||
#[cfg(test)]
|
||||
mod tests_fx_color;
|
||||
#[cfg(test)]
|
||||
mod tests_fx_distort;
|
||||
#[cfg(test)]
|
||||
mod tests_fx_generate;
|
||||
#[cfg(test)]
|
||||
mod tests_particles;
|
||||
|
|
|
|||
|
|
@ -134,6 +134,17 @@ pub fn channel_mix(e: &mut Enc, canvas: &GpuImage, tmp: &GpuImage, channels: [bo
|
|||
out
|
||||
}
|
||||
|
||||
/// Adjustment layer finish: move `canvas` toward `adjusted` by `matte`'s alpha × `opacity`.
|
||||
pub fn adjust_mix(e: &mut Enc, canvas: &GpuImage, adjusted: &GpuImage, matte: &GpuImage, opacity: f32) -> GpuImage {
|
||||
let (rows, stride) = e.image_rows(matte);
|
||||
let mut p = Params::default();
|
||||
p.u[0][0] = stride;
|
||||
p.f[0][0] = opacity;
|
||||
let out = e.image(canvas.width, canvas.height);
|
||||
e.pixels("adjust_mix", &p, canvas, Some(adjusted), &out, Some(&rows));
|
||||
out
|
||||
}
|
||||
|
||||
/// Clamp and quantise to `levels` steps (8/16 bpc).
|
||||
pub fn quantize(e: &mut Enc, img: &GpuImage, levels: f32) -> GpuImage {
|
||||
let mut p = Params::default();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,209 @@
|
|||
//! GPU particles (`particles.wgsl`): the stepped particle systems of `effectcraft-effects`
|
||||
//! ([`effectcraft_effects::psim`]) simulated with one invocation per particle. States stay on
|
||||
//! the GPU as checkpoints (per system key, like the CPU's `SimCache`), so playback advances from
|
||||
//! the previous frame's state instead of simulating from layer time 0.
|
||||
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
|
||||
use effectcraft_effects::psim::{ParticleSystem, SimParticle, SimRequest};
|
||||
use wgpu::util::DeviceExt;
|
||||
|
||||
use crate::context::GpuContext;
|
||||
|
||||
/// Bytes of one particle state (`PState` in the shader).
|
||||
const STATE_BYTES: u64 = 48;
|
||||
/// Checkpoints kept (all keys).
|
||||
const CHECKPOINTS: usize = 24;
|
||||
|
||||
pub(crate) struct Pipes {
|
||||
bgl: wgpu::BindGroupLayout,
|
||||
pipe: wgpu::ComputePipeline,
|
||||
}
|
||||
|
||||
struct Checkpoint {
|
||||
key: u64,
|
||||
steps: u64,
|
||||
n: u32,
|
||||
buf: wgpu::Buffer,
|
||||
used: u64,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct Checkpoints {
|
||||
list: Vec<Checkpoint>,
|
||||
clock: u64,
|
||||
}
|
||||
|
||||
fn pipes(g: &GpuContext) -> &Pipes {
|
||||
g.particles.get_or_init(|| {
|
||||
let d = &g.device;
|
||||
let src = [include_str!("shaders/common.wgsl"), include_str!("shaders/kernels.wgsl"), include_str!("shaders/particles.wgsl")].concat();
|
||||
let module =
|
||||
d.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("effectcraft particles"), source: wgpu::ShaderSource::Wgsl(src.into()) });
|
||||
let buffer = |binding: u32, ty: wgpu::BufferBindingType| wgpu::BindGroupLayoutEntry {
|
||||
binding,
|
||||
visibility: wgpu::ShaderStages::COMPUTE,
|
||||
ty: wgpu::BindingType::Buffer { ty, has_dynamic_offset: false, min_binding_size: None },
|
||||
count: None,
|
||||
};
|
||||
let bgl = d.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("effectcraft particles"),
|
||||
entries: &[
|
||||
buffer(0, wgpu::BufferBindingType::Uniform),
|
||||
buffer(1, wgpu::BufferBindingType::Storage { read_only: true }),
|
||||
buffer(2, wgpu::BufferBindingType::Storage { read_only: true }),
|
||||
buffer(3, wgpu::BufferBindingType::Storage { read_only: false }),
|
||||
],
|
||||
});
|
||||
let layout = d.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("effectcraft particles"),
|
||||
bind_group_layouts: &[None, Some(&bgl)],
|
||||
immediate_size: 0,
|
||||
});
|
||||
let pipe = d.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
|
||||
label: Some("particles"),
|
||||
layout: Some(&layout),
|
||||
module: &module,
|
||||
entry_point: Some("particles"),
|
||||
compilation_options: Default::default(),
|
||||
cache: None,
|
||||
});
|
||||
Pipes { bgl, pipe }
|
||||
})
|
||||
}
|
||||
|
||||
/// The shader's parameter block.
|
||||
fn uniforms(req: &SimRequest, n: u32, s0: u64, prev_n: u32) -> Vec<u8> {
|
||||
let dt = (1.0 / effectcraft_effects::psim::SPS) as f32;
|
||||
let mut u = [n, s0 as u32, req.steps as u32, 0];
|
||||
let u2;
|
||||
let mut f = [[0.0f32; 4]; 6];
|
||||
match &req.system {
|
||||
ParticleSystem::Engine(e) => {
|
||||
u2 = [e.anim, e.flat as u32, e.seed, prev_n];
|
||||
f[0] = [e.life, e.speed, e.extra, e.cone];
|
||||
f[1] = [e.producer[0], e.producer[1], e.producer[2], 0.0];
|
||||
f[2] = [e.radius[0], e.radius[1], e.radius[2], 0.0];
|
||||
f[3] = [e.gravity[0], e.gravity[1], e.gravity[2], 0.0];
|
||||
f[4] = [e.axis[0], e.axis[1], e.axis[2], 0.0];
|
||||
f[5] = [e.damp, dt, 0.0, 0.0];
|
||||
}
|
||||
ParticleSystem::Cannon(c) => {
|
||||
u[3] = 1;
|
||||
u2 = [0, 1, c.seed, prev_n];
|
||||
f[0] = [c.pos[0], c.pos[1], c.barrel_angle, c.barrel_radius];
|
||||
f[1] = [c.dir_spread, c.vel, c.vel_spread, c.grav_spread];
|
||||
f[2] = [c.gravity[0], c.gravity[1], dt, 0.0];
|
||||
f[3] = c.bounds;
|
||||
}
|
||||
}
|
||||
let mut out = Vec::with_capacity(128);
|
||||
for x in u.iter().chain(&u2) {
|
||||
out.extend_from_slice(&x.to_le_bytes());
|
||||
}
|
||||
for x in f.iter().flatten() {
|
||||
out.extend_from_slice(&x.to_le_bytes());
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Simulate `req` (see [`effectcraft_effects::psim::ParticleSim`]).
|
||||
pub(crate) fn simulate(g: &GpuContext, req: &SimRequest) -> Option<Vec<SimParticle>> {
|
||||
if !g.can_readback() || req.steps > u32::MAX as u64 {
|
||||
return None;
|
||||
}
|
||||
let n = req.births.len() as u32;
|
||||
if n == 0 {
|
||||
return Some(vec![]);
|
||||
}
|
||||
let size = n as u64 * STATE_BYTES;
|
||||
if size > g.device.limits().max_storage_buffer_binding_size {
|
||||
return None;
|
||||
}
|
||||
let p = pipes(g);
|
||||
// The latest checkpoint at or before the target step.
|
||||
let (s0, prev, prev_n) = {
|
||||
let mut c = g.particle_states.lock().ok()?;
|
||||
c.clock += 1;
|
||||
let now = c.clock;
|
||||
match c.list.iter_mut().filter(|k| k.key == req.key && k.steps <= req.steps).max_by_key(|k| k.steps) {
|
||||
Some(k) => {
|
||||
k.used = now;
|
||||
(k.steps, Some(k.buf.clone()), k.n)
|
||||
}
|
||||
None => (0, None, 0),
|
||||
}
|
||||
};
|
||||
let d = &g.device;
|
||||
let births: Vec<u8> = req.births.iter().flat_map(|b| b.to_le_bytes()).collect();
|
||||
let births = d.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("births"), contents: &births, usage: wgpu::BufferUsages::STORAGE });
|
||||
let prev = prev.unwrap_or_else(|| {
|
||||
d.create_buffer(&wgpu::BufferDescriptor { label: Some("particles"), size: STATE_BYTES, usage: wgpu::BufferUsages::STORAGE, mapped_at_creation: false })
|
||||
});
|
||||
let next = d.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("particles"),
|
||||
size,
|
||||
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
let ub = d.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("particles"),
|
||||
contents: &uniforms(req, n, s0, prev_n),
|
||||
usage: wgpu::BufferUsages::UNIFORM,
|
||||
});
|
||||
let bg = d.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: None,
|
||||
layout: &p.bgl,
|
||||
entries: &[
|
||||
wgpu::BindGroupEntry { binding: 0, resource: ub.as_entire_binding() },
|
||||
wgpu::BindGroupEntry { binding: 1, resource: births.as_entire_binding() },
|
||||
wgpu::BindGroupEntry { binding: 2, resource: prev.as_entire_binding() },
|
||||
wgpu::BindGroupEntry { binding: 3, resource: next.as_entire_binding() },
|
||||
],
|
||||
});
|
||||
let read = d.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("particles readback"),
|
||||
size,
|
||||
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
let mut enc = d.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("particles") });
|
||||
{
|
||||
let mut pass = enc.begin_compute_pass(&wgpu::ComputePassDescriptor { label: Some("particles"), timestamp_writes: None });
|
||||
pass.set_pipeline(&p.pipe);
|
||||
pass.set_bind_group(1, &bg, &[]);
|
||||
pass.dispatch_workgroups(n.div_ceil(64), 1, 1);
|
||||
}
|
||||
enc.copy_buffer_to_buffer(&next, 0, &read, 0, size);
|
||||
g.queue.submit([enc.finish()]);
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
read.map_async(wgpu::MapMode::Read, .., move |r| {
|
||||
let _ = tx.send(r);
|
||||
});
|
||||
g.device.poll(wgpu::PollType::wait_indefinitely()).ok()?;
|
||||
rx.recv().ok()?.ok()?;
|
||||
let view = read.get_mapped_range(..).ok()?;
|
||||
let f: Vec<f32> = view.chunks_exact(4).map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]])).collect();
|
||||
drop(view);
|
||||
read.unmap();
|
||||
let parts = f
|
||||
.chunks_exact(12)
|
||||
.enumerate()
|
||||
.filter(|(_, s)| s[9] != 0.0)
|
||||
.map(|(id, s)| SimParticle { p: [s[0], s[1], s[2]], v: [s[4], s[5], s[6]], age: s[3], life: s[7], rnd: s[8], id: id as u32 })
|
||||
.collect();
|
||||
// Keep the new state as a checkpoint.
|
||||
if let Ok(mut c) = g.particle_states.lock() {
|
||||
let now = c.clock;
|
||||
c.list.retain(|k| !(k.key == req.key && k.steps == req.steps));
|
||||
c.list.push(Checkpoint { key: req.key, steps: req.steps, n, buf: next, used: now });
|
||||
while c.list.len() > CHECKPOINTS {
|
||||
let Some(i) = c.list.iter().enumerate().min_by_key(|(_, k)| k.used).map(|(i, _)| i) else { break };
|
||||
c.list.remove(i);
|
||||
}
|
||||
}
|
||||
Some(parts)
|
||||
}
|
||||
|
||||
pub(crate) type PipesCell = OnceLock<Pipes>;
|
||||
pub(crate) type StatesCell = Mutex<Checkpoints>;
|
||||
|
|
@ -0,0 +1,460 @@
|
|||
// Classic 3D on the GPU: the per-pixel compositor of `render::three_d::compose` (draw_run).
|
||||
//
|
||||
// Every plane covering an output pixel is inverse-mapped through its homography (one per
|
||||
// motion-blur sub-sample), sampled from the plane atlas (`src`), shaded by the comp's lights
|
||||
// (Blinn-Phong, ray-cast shadows against the caster planes) and multiplied by its track matte;
|
||||
// the pixel's fragments are insertion-sorted far → near (coplanar fragments in stack order) and
|
||||
// blended over the canvas (`aux`) with each layer's mode and opacity.
|
||||
//
|
||||
// Bindings: group 0 as every kernel (P, src = atlas, aux = canvas, out, data = track mattes);
|
||||
// group 1 = planes, geometries, lights, caster indices.
|
||||
// P.u[0] = (planes, lights, casters, _)
|
||||
|
||||
struct Plane {
|
||||
// Atlas rectangle (x, y, w, h) in texels.
|
||||
atlas: vec4<f32>,
|
||||
// Buffer offset (x, y), scale, bicubic (0 / 1).
|
||||
tex: vec4<f32>,
|
||||
// Output pixel bounds [x0, y0, x1, y1).
|
||||
bbox: vec4<f32>,
|
||||
// Layer-space bounds of the buffer.
|
||||
bounds: vec4<f32>,
|
||||
// World → layer (affine rows).
|
||||
winv0: vec4<f32>,
|
||||
winv1: vec4<f32>,
|
||||
winv2: vec4<f32>,
|
||||
// Ambient, diffuse, specular, Blinn-Phong exponent.
|
||||
mat0: vec4<f32>,
|
||||
// Metal, light transmission, opacity.
|
||||
mat1: vec4<f32>,
|
||||
// First geometry, geometry count, flags (1 draw, 2 preserve transparency, 4 accepts
|
||||
// lights), blend mode.
|
||||
ints: vec4<u32>,
|
||||
// Casts shadows, accepts shadows (0 off, 1 on, 2 only), layer id, stack order.
|
||||
ints2: vec4<u32>,
|
||||
// Track matte (index + 1; 0 = none), dissolve seed.
|
||||
ints3: vec4<u32>,
|
||||
}
|
||||
|
||||
struct Geo {
|
||||
// Output pixel → homogeneous layer coordinates (rows).
|
||||
h0: vec4<f32>,
|
||||
h1: vec4<f32>,
|
||||
h2: vec4<f32>,
|
||||
// Camera depth = d.x·u + d.y·v + d.z.
|
||||
depth: vec4<f32>,
|
||||
// Layer → world (affine rows).
|
||||
w0: vec4<f32>,
|
||||
w1: vec4<f32>,
|
||||
w2: vec4<f32>,
|
||||
// Unit world normal.
|
||||
normal: vec4<f32>,
|
||||
// Camera eye (xyz) and orthographic flag (w).
|
||||
eye: vec4<f32>,
|
||||
// Camera forward (xyz).
|
||||
fwd: vec4<f32>,
|
||||
}
|
||||
|
||||
struct Light {
|
||||
// Kind (0 ambient, 1 parallel, 2 point, 3 spot), falloff, casts shadows.
|
||||
ints: vec4<u32>,
|
||||
pos: vec4<f32>,
|
||||
dir: vec4<f32>,
|
||||
color: vec4<f32>,
|
||||
// Cone half-angle, feather, radius, falloff distance.
|
||||
cone: vec4<f32>,
|
||||
// Shadow darkness, shadow diffusion.
|
||||
shadow: vec4<f32>,
|
||||
}
|
||||
|
||||
@group(1) @binding(0) var<storage, read> planes: array<Plane>;
|
||||
@group(1) @binding(1) var<storage, read> geos: array<Geo>;
|
||||
@group(1) @binding(2) var<storage, read> lights: array<Light>;
|
||||
@group(1) @binding(3) var<storage, read> casters: array<u32>;
|
||||
|
||||
const NEAR3: f32 = 1.0;
|
||||
const MAX_FRAGS: u32 = 32u;
|
||||
|
||||
fn atlas_get(i: u32, x: i32, y: i32) -> vec4<f32> {
|
||||
let r = planes[i].atlas;
|
||||
if (x < 0 || y < 0 || x >= i32(r.z) || y >= i32(r.w)) {
|
||||
return vec4<f32>(0.0);
|
||||
}
|
||||
return textureLoad(src, vec2<i32>(i32(r.x) + x, i32(r.y) + y), 0);
|
||||
}
|
||||
|
||||
// Image::sample_bilinear / sample_bicubic on a plane's buffer at layer coordinates (u, v).
|
||||
fn texel(i: u32, u: f32, v: f32) -> vec4<f32> {
|
||||
let t = planes[i].tex;
|
||||
let x = u * t.z + t.x;
|
||||
let y = v * t.z + t.y;
|
||||
let fx = x - 0.5;
|
||||
let fy = y - 0.5;
|
||||
let x0 = floor(fx);
|
||||
let y0 = floor(fy);
|
||||
let xi = i32(x0);
|
||||
let yi = i32(y0);
|
||||
if (t.w > 0.5) {
|
||||
let wx = cubic_w(fx - x0);
|
||||
let wy = cubic_w(fy - y0);
|
||||
var o = vec4<f32>(0.0);
|
||||
for (var j = 0; j < 4; j++) {
|
||||
for (var k = 0; k < 4; k++) {
|
||||
o += atlas_get(i, xi - 1 + k, yi - 1 + j) * (wx[k] * wy[j]);
|
||||
}
|
||||
}
|
||||
return max(o, vec4<f32>(0.0));
|
||||
}
|
||||
let tx = fx - x0;
|
||||
let ty = fy - y0;
|
||||
let a = atlas_get(i, xi, yi);
|
||||
let b = atlas_get(i, xi + 1, yi);
|
||||
let c = atlas_get(i, xi, yi + 1);
|
||||
let d = atlas_get(i, xi + 1, yi + 1);
|
||||
let top = a + (b - a) * tx;
|
||||
let bot = c + (d - c) * tx;
|
||||
return top + (bot - top) * ty;
|
||||
}
|
||||
|
||||
fn affine(r0: vec4<f32>, r1: vec4<f32>, r2: vec4<f32>, p: vec3<f32>) -> vec3<f32> {
|
||||
return vec3<f32>(dot(r0.xyz, p) + r0.w, dot(r1.xyz, p) + r1.w, dot(r2.xyz, p) + r2.w);
|
||||
}
|
||||
|
||||
fn linear3(r0: vec4<f32>, r1: vec4<f32>, r2: vec4<f32>, v: vec3<f32>) -> vec3<f32> {
|
||||
return vec3<f32>(dot(r0.xyz, v), dot(r1.xyz, v), dot(r2.xyz, v));
|
||||
}
|
||||
|
||||
// compose::unproject: (u, v, depth, ok).
|
||||
fn unproject(g: u32, sx: f32, sy: f32) -> vec4<f32> {
|
||||
let gm = geos[g];
|
||||
let ortho = gm.eye.w > 0.5;
|
||||
let qz = gm.h2.x * sx + gm.h2.y * sy + gm.h2.z;
|
||||
if ((!ortho && qz <= 0.0) || qz == 0.0) {
|
||||
return vec4<f32>(0.0);
|
||||
}
|
||||
let u = (gm.h0.x * sx + gm.h0.y * sy + gm.h0.z) / qz;
|
||||
let v = (gm.h1.x * sx + gm.h1.y * sy + gm.h1.z) / qz;
|
||||
let z = gm.depth.x * u + gm.depth.y * v + gm.depth.z;
|
||||
if (!ortho && z < NEAR3) {
|
||||
return vec4<f32>(0.0);
|
||||
}
|
||||
return vec4<f32>(u, v, z, 1.0);
|
||||
}
|
||||
|
||||
// LightState::to_light: unit vector towards the light (xyz) and the distance (w; -1 = ∞).
|
||||
fn to_light(l: u32, p: vec3<f32>) -> vec4<f32> {
|
||||
let li = lights[l];
|
||||
if (li.ints.x == 1u) {
|
||||
return vec4<f32>(-li.dir.xyz, -1.0);
|
||||
}
|
||||
let d = li.pos.xyz - p;
|
||||
let len = length(d);
|
||||
if (len < 1e-9) {
|
||||
return vec4<f32>(0.0, 0.0, -1.0, 0.0);
|
||||
}
|
||||
return vec4<f32>(d / len, len);
|
||||
}
|
||||
|
||||
fn smoothstep01(t: f32) -> f32 {
|
||||
return 1.0 - t * t * (3.0 - 2.0 * t);
|
||||
}
|
||||
|
||||
// LightState::attenuation.
|
||||
fn attenuation(l: u32, p: vec3<f32>) -> f32 {
|
||||
let li = lights[l];
|
||||
let tl = to_light(l, p);
|
||||
let dist = tl.w;
|
||||
let finite = dist >= 0.0;
|
||||
let radius = li.cone.z;
|
||||
let fd = li.cone.w;
|
||||
var k = 1.0;
|
||||
if (li.ints.y == 1u && finite) {
|
||||
if (dist <= radius) {
|
||||
k = 1.0;
|
||||
} else if (fd <= 0.0) {
|
||||
k = 0.0;
|
||||
} else {
|
||||
k = smoothstep01(clamp((dist - radius) / fd, 0.0, 1.0));
|
||||
}
|
||||
} else if (li.ints.y == 2u && finite) {
|
||||
if (dist <= radius || (radius <= 0.0 && dist <= 1.0)) {
|
||||
k = 1.0;
|
||||
} else {
|
||||
let q = max(radius, 1.0) / dist;
|
||||
k = q * q;
|
||||
}
|
||||
}
|
||||
if (li.ints.x == 3u) {
|
||||
let cos_a = clamp(dot(-tl.xyz, li.dir.xyz), -1.0, 1.0);
|
||||
let a = acos(cos_a);
|
||||
let outer = li.cone.x;
|
||||
let inner = outer * (1.0 - li.cone.y);
|
||||
if (a <= inner) {
|
||||
} else if (a >= outer) {
|
||||
k = 0.0;
|
||||
} else {
|
||||
k *= smoothstep01((a - inner) / max(outer - inner, 1e-9));
|
||||
}
|
||||
}
|
||||
return k;
|
||||
}
|
||||
|
||||
// compose::occlusion: RGB transmittance of light `l` reaching `p` past caster `c`.
|
||||
fn occlusion(c: u32, p: vec3<f32>, l: u32) -> vec3<f32> {
|
||||
let pl = planes[c];
|
||||
let li = lights[l];
|
||||
let a = affine(pl.winv0, pl.winv1, pl.winv2, p);
|
||||
var hit: vec3<f32>;
|
||||
var d_pc: f32;
|
||||
var d_cl: f32;
|
||||
if (li.ints.x == 1u) {
|
||||
// Towards the light, 1e6 away (in direction form: exact in f32).
|
||||
let dl = linear3(pl.winv0, pl.winv1, pl.winv2, -li.dir.xyz);
|
||||
if (dl.z == 0.0) {
|
||||
return vec3<f32>(1.0);
|
||||
}
|
||||
let s = -a.z / dl.z;
|
||||
if (s <= 1.0 || s >= 1.0e6) {
|
||||
return vec3<f32>(1.0);
|
||||
}
|
||||
hit = a + dl * s;
|
||||
d_pc = length(li.dir.xyz) * s;
|
||||
d_cl = 1000.0;
|
||||
} else {
|
||||
let b = affine(pl.winv0, pl.winv1, pl.winv2, li.pos.xyz);
|
||||
if (a.z == b.z || (a.z < 0.0) == (b.z < 0.0)) {
|
||||
return vec3<f32>(1.0);
|
||||
}
|
||||
let t = a.z / (a.z - b.z);
|
||||
if (t <= 1e-6 || t >= 1.0) {
|
||||
return vec3<f32>(1.0);
|
||||
}
|
||||
hit = a + (b - a) * t;
|
||||
let seg = length(li.pos.xyz - p);
|
||||
d_pc = seg * t;
|
||||
d_cl = max(seg * (1.0 - t), 1.0);
|
||||
}
|
||||
let r = li.shadow.y * min(d_pc / d_cl, 4.0);
|
||||
let bd = pl.bounds;
|
||||
if (hit.x < bd.x - r || hit.y < bd.y - r || hit.x > bd.z + r || hit.y > bd.w + r) {
|
||||
return vec3<f32>(1.0);
|
||||
}
|
||||
var px = vec4<f32>(0.0);
|
||||
if (r * pl.tex.z < 0.5) {
|
||||
px = texel(c, hit.x, hit.y);
|
||||
} else {
|
||||
for (var j = -1; j <= 1; j++) {
|
||||
for (var i = -1; i <= 1; i++) {
|
||||
px += texel(c, hit.x + f32(i) * r * 0.66, hit.y + f32(j) * r * 0.66) / 9.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
let alpha = clamp(px.w * pl.mat1.z, 0.0, 1.0);
|
||||
if (alpha <= 0.0) {
|
||||
return vec3<f32>(1.0);
|
||||
}
|
||||
let tr = pl.mat1.y;
|
||||
let dark = li.shadow.x;
|
||||
var o = vec3<f32>(1.0);
|
||||
for (var k = 0; k < 3; k++) {
|
||||
var st = 0.0;
|
||||
if (px.w > 0.0) {
|
||||
st = clamp(px[k] / px.w, 0.0, 1.0);
|
||||
}
|
||||
let through = (1.0 - alpha) + alpha * tr * st;
|
||||
o[k] = 1.0 - dark * (1.0 - through);
|
||||
}
|
||||
return o;
|
||||
}
|
||||
|
||||
fn shadow_of(it: u32, l: u32, p: vec3<f32>) -> vec3<f32> {
|
||||
var tr = vec3<f32>(1.0);
|
||||
let n = P.u[0].z;
|
||||
let lid = planes[it].ints2.z;
|
||||
for (var k = 0u; k < n; k++) {
|
||||
let c = casters[k];
|
||||
if (c == it || planes[c].ints2.z == lid) {
|
||||
continue;
|
||||
}
|
||||
tr *= occlusion(c, p, l);
|
||||
}
|
||||
return tr;
|
||||
}
|
||||
|
||||
// compose::shade_texel (light::shade).
|
||||
fn shade_texel(it: u32, g: u32, u: f32, v: f32, t: vec4<f32>) -> vec4<f32> {
|
||||
let pl = planes[it];
|
||||
let n_lights = P.u[0].y;
|
||||
let accepts_shadows = pl.ints2.y;
|
||||
if (n_lights == 0u || (pl.ints.z & 4u) == 0u) {
|
||||
if (accepts_shadows == 2u) {
|
||||
return vec4<f32>(0.0);
|
||||
}
|
||||
return t;
|
||||
}
|
||||
let gm = geos[g];
|
||||
let a = t.w;
|
||||
let rgb = t.xyz / a;
|
||||
let p = affine(gm.w0, gm.w1, gm.w2, vec3<f32>(u, v, 0.0));
|
||||
var to_viewer: vec3<f32>;
|
||||
if (gm.eye.w > 0.5) {
|
||||
to_viewer = -gm.fwd.xyz;
|
||||
} else {
|
||||
to_viewer = normalize(gm.eye.xyz - p);
|
||||
}
|
||||
var n = gm.normal.xyz;
|
||||
if (dot(n, to_viewer) < 0.0) {
|
||||
n = -n;
|
||||
}
|
||||
var amb = vec3<f32>(0.0);
|
||||
var diff = vec3<f32>(0.0);
|
||||
var spec = vec3<f32>(0.0);
|
||||
var shadow_amt = 0.0;
|
||||
let ex = pl.mat0.w;
|
||||
let metal = pl.mat1.x;
|
||||
let transmission = pl.mat1.y;
|
||||
let specular = pl.mat0.z;
|
||||
for (var i = 0u; i < n_lights; i++) {
|
||||
let li = lights[i];
|
||||
if (li.ints.x == 0u) {
|
||||
amb += li.color.xyz;
|
||||
continue;
|
||||
}
|
||||
let k = attenuation(i, p);
|
||||
if (k <= 0.0) {
|
||||
continue;
|
||||
}
|
||||
let lv = to_light(i, p).xyz;
|
||||
let ndl = dot(n, lv);
|
||||
let front = ndl > 0.0;
|
||||
var lambert = ndl;
|
||||
if (!front) {
|
||||
lambert = -ndl * transmission;
|
||||
}
|
||||
if (lambert <= 0.0) {
|
||||
continue;
|
||||
}
|
||||
var sh = vec3<f32>(1.0);
|
||||
if (li.ints.z != 0u && accepts_shadows != 0u) {
|
||||
sh = shadow_of(it, i, p);
|
||||
}
|
||||
shadow_amt = max(shadow_amt, 1.0 - (sh.x + sh.y + sh.z) / 3.0);
|
||||
diff += li.color.xyz * (lambert * k) * sh;
|
||||
if (front && specular > 0.0) {
|
||||
let h = normalize(lv + to_viewer);
|
||||
let s = powz(max(dot(n, h), 0.0), ex) * k;
|
||||
let tint = vec3<f32>(1.0) + (rgb - vec3<f32>(1.0)) * metal;
|
||||
spec += li.color.xyz * s * sh * tint;
|
||||
}
|
||||
}
|
||||
let lit = rgb * (pl.mat0.x * amb + pl.mat0.y * diff) + specular * spec;
|
||||
if (accepts_shadows == 2u) {
|
||||
return vec4<f32>(0.0, 0.0, 0.0, a * shadow_amt);
|
||||
}
|
||||
return vec4<f32>(lit * a, a);
|
||||
}
|
||||
|
||||
// compose::before: a draws before b (farther; coplanar → lower stack order first).
|
||||
fn before(za: f32, oa: u32, zb: f32, ob: u32) -> bool {
|
||||
let tol = 1e-5 * max(max(abs(za), abs(zb)), 1.0);
|
||||
if (abs(za - zb) <= tol) {
|
||||
return oa < ob;
|
||||
}
|
||||
return za > zb;
|
||||
}
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn classic3d(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let dims = out_dims();
|
||||
let x = i32(gid.x);
|
||||
let y = i32(gid.y);
|
||||
if (x >= dims.x || y >= dims.y) {
|
||||
return;
|
||||
}
|
||||
let pix = vec2<i32>(x, y);
|
||||
var d = textureLoad(aux, pix, 0);
|
||||
let fx = f32(x);
|
||||
let fy = f32(y);
|
||||
let sx = fx + 0.5;
|
||||
let sy = fy + 0.5;
|
||||
var fz: array<f32, MAX_FRAGS>;
|
||||
var fo: array<u32, MAX_FRAGS>;
|
||||
var fi: array<u32, MAX_FRAGS>;
|
||||
var fp: array<vec4<f32>, MAX_FRAGS>;
|
||||
var nf = 0u;
|
||||
let n_planes = P.u[0].x;
|
||||
for (var i = 0u; i < n_planes; i++) {
|
||||
let pl = planes[i];
|
||||
if ((pl.ints.z & 1u) == 0u) {
|
||||
continue;
|
||||
}
|
||||
if (fx < pl.bbox.x || fx >= pl.bbox.z || fy < pl.bbox.y || fy >= pl.bbox.w) {
|
||||
continue;
|
||||
}
|
||||
// compose::fragment: average of the motion-blur sub-samples.
|
||||
let g0 = pl.ints.x;
|
||||
let ng = pl.ints.y;
|
||||
var acc = vec4<f32>(0.0);
|
||||
var depth = 0.0;
|
||||
var hit = false;
|
||||
for (var k = 0u; k < ng; k++) {
|
||||
let q = unproject(g0 + k, sx, sy);
|
||||
if (q.w == 0.0) {
|
||||
continue;
|
||||
}
|
||||
let t = texel(i, q.x, q.y);
|
||||
if (t.w <= 1e-6) {
|
||||
continue;
|
||||
}
|
||||
if (!hit) {
|
||||
depth = q.z;
|
||||
hit = true;
|
||||
}
|
||||
acc += shade_texel(i, g0 + k, q.x, q.y, t);
|
||||
}
|
||||
if (!hit) {
|
||||
continue;
|
||||
}
|
||||
if (ng > 1u) {
|
||||
acc *= 1.0 / f32(ng);
|
||||
}
|
||||
let m = pl.ints3.x;
|
||||
if (m != 0u) {
|
||||
let f = data[(m - 1u) * u32(dims.x) * u32(dims.y) + u32(y) * u32(dims.x) + u32(x)];
|
||||
acc *= f;
|
||||
}
|
||||
if (!(acc.w > 0.0 || acc.x != 0.0 || acc.y != 0.0 || acc.z != 0.0) || nf >= MAX_FRAGS) {
|
||||
continue;
|
||||
}
|
||||
// Insertion: far → near, coplanar in stack order (stable, as the CPU's insertion sort).
|
||||
let order = pl.ints2.w;
|
||||
var j = nf;
|
||||
while (j > 0u && before(depth, order, fz[j - 1u], fo[j - 1u])) {
|
||||
fz[j] = fz[j - 1u];
|
||||
fo[j] = fo[j - 1u];
|
||||
fi[j] = fi[j - 1u];
|
||||
fp[j] = fp[j - 1u];
|
||||
j--;
|
||||
}
|
||||
fz[j] = depth;
|
||||
fo[j] = order;
|
||||
fi[j] = i;
|
||||
fp[j] = acc;
|
||||
nf++;
|
||||
}
|
||||
for (var k = 0u; k < nf; k++) {
|
||||
let pl = planes[fi[k]];
|
||||
var op = pl.mat1.z;
|
||||
if ((pl.ints.z & 2u) != 0u) {
|
||||
op *= d.w;
|
||||
}
|
||||
let s = fp[k] * op;
|
||||
let mode = pl.ints.w;
|
||||
var n = 0.5;
|
||||
if (mode == 1u || mode == 2u) {
|
||||
n = hash_noise(u32(x), u32(y), pl.ints3.y);
|
||||
}
|
||||
d = blend_pixel(mode, d, s, n);
|
||||
}
|
||||
textureStore(out, pix, d);
|
||||
}
|
||||
|
|
@ -0,0 +1,707 @@
|
|||
// GPU effects (color family): see src/fx_color.rs. Each entry point mirrors the CPU effect in
|
||||
// effectcraft-effects operation for operation; parameter layouts are documented per entry.
|
||||
// Every name here is prefixed `fxc_` (the family files share one module).
|
||||
|
||||
// ---------------------------------------------------------------- helpers
|
||||
|
||||
// util::unpremul: straight colour (0 when alpha ≤ 1e-6).
|
||||
fn fxc_unpremul(px: vec4<f32>) -> vec3<f32> {
|
||||
if (px.w > 1e-6) {
|
||||
return px.xyz / px.w;
|
||||
}
|
||||
return vec3<f32>(0.0);
|
||||
}
|
||||
|
||||
// util::smoothstep.
|
||||
fn fxc_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);
|
||||
}
|
||||
|
||||
fn fxc_mix(a: vec3<f32>, b: vec3<f32>, t: f32) -> vec3<f32> {
|
||||
return a + (b - a) * t;
|
||||
}
|
||||
|
||||
// color::hsv_to_rgb.
|
||||
fn fxc_hsv_to_rgb(h: f32, s: f32, v: f32) -> vec3<f32> {
|
||||
let h6 = fract_euclid(h) * 6.0;
|
||||
let i = floor(h6);
|
||||
let f = h6 - i;
|
||||
let p = v * (1.0 - s);
|
||||
let q = v * (1.0 - s * f);
|
||||
let t = v * (1.0 - s * (1.0 - f));
|
||||
switch i32(i) {
|
||||
case 0: { return vec3<f32>(v, t, p); }
|
||||
case 1: { return vec3<f32>(q, v, p); }
|
||||
case 2: { return vec3<f32>(p, v, t); }
|
||||
case 3: { return vec3<f32>(p, q, v); }
|
||||
case 4: { return vec3<f32>(t, p, v); }
|
||||
default: { return vec3<f32>(v, p, q); }
|
||||
}
|
||||
}
|
||||
|
||||
fn fxc_pixel(gid: vec3<u32>) -> vec2<i32> {
|
||||
let dims = out_dims();
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (p.x >= dims.x || p.y >= dims.y) {
|
||||
return vec2<i32>(-1);
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- per-pixel colour effects
|
||||
|
||||
// Colorama's input phase (color_fx::colorama_phase).
|
||||
fn fxc_colorama_phase(c: vec3<f32>, a: f32, mode: u32) -> f32 {
|
||||
var v: f32;
|
||||
switch mode {
|
||||
case 1u: { v = c.x; }
|
||||
case 2u: { v = c.y; }
|
||||
case 3u: { v = c.z; }
|
||||
case 4u: { v = rgb_to_hsl(c).x; }
|
||||
case 5u: { v = rgb_to_hsl(c).z; }
|
||||
case 6u: { v = rgb_to_hsl(c).y; }
|
||||
case 7u: { v = max(max(c.x, c.y), c.z); }
|
||||
case 8u: { v = a; }
|
||||
case 9u: { v = 0.0; }
|
||||
default: { v = luminance(c); }
|
||||
}
|
||||
return clamp(v, 0.0, 1.0);
|
||||
}
|
||||
|
||||
// Selective Color range weights (color2::sc_weights).
|
||||
fn fxc_sc_weights(c0: vec3<f32>) -> array<f32, 9> {
|
||||
let c = clamp(c0, vec3<f32>(0.0), vec3<f32>(1.0));
|
||||
// Stable descending sort of the channel indices.
|
||||
var idx = vec3<u32>(0u, 1u, 2u);
|
||||
for (var i = 1u; i < 3u; i++) {
|
||||
var j = i;
|
||||
while (j > 0u && c[idx[j]] > c[idx[j - 1u]]) {
|
||||
let t = idx[j];
|
||||
idx[j] = idx[j - 1u];
|
||||
idx[j - 1u] = t;
|
||||
j--;
|
||||
}
|
||||
}
|
||||
let mx = c[idx.x];
|
||||
let md = c[idx.y];
|
||||
let mn = c[idx.z];
|
||||
var w = array<f32, 9>(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
|
||||
var prim = array<u32, 3>(0u, 2u, 4u);
|
||||
var sec = array<u32, 3>(3u, 5u, 1u);
|
||||
w[prim[idx.x]] = mx - md;
|
||||
w[sec[idx.z]] = md - mn;
|
||||
w[6] = max((mn - 0.5) * 2.0, 0.0);
|
||||
w[8] = max((0.5 - mx) * 2.0, 0.0);
|
||||
w[7] = clamp(1.0 - (abs(mx - 0.5) + abs(mn - 0.5)), 0.0, 1.0);
|
||||
return w;
|
||||
}
|
||||
|
||||
// u[0].x = op:
|
||||
// 1 Color Balance f[0] = shadows, f[1] = midtones, f[2] = highlights; u[0].y = preserve luminosity
|
||||
// 2 Vibrance f[0] = (vibrance, saturation)
|
||||
// 3 Black & White f[0] = (reds, yellows, greens, cyans), f[1] = (blues, magentas);
|
||||
// u[0].y = tint; f[2] = (tint hue, tint saturation)
|
||||
// 4 Tritone f[0] = highlights, f[1] = midtones, f[2] = shadows; f[3].x = 1 − blend
|
||||
// 5 Colorama u[0].y = phase mode, u[0].z = interpolate; f[0] = (cycles, shift, 1 − blend)
|
||||
// 6 Channel Mixer f[0..3] = output rows (r, g, b, const); u[0].y = monochrome
|
||||
// 7 Selective Color data = 9 × (cyan, magenta, yellow, black); u[0].y = relative
|
||||
// 8 Linear Color Key f[0] = (key rgb, tolerance), f[1] = (softness, key chroma xy, key hue);
|
||||
// u[0] = (_, match mode, keep colours, view)
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxc_point(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = fxc_pixel(gid);
|
||||
if (p.x < 0) {
|
||||
return;
|
||||
}
|
||||
var px = textureLoad(src, p, 0);
|
||||
let a = px.w;
|
||||
switch P.u[0].x {
|
||||
case 1u: {
|
||||
if (a > 0.0) {
|
||||
let c = px.xyz / a;
|
||||
let l = clamp(luminance(c), 0.0, 1.0);
|
||||
let ws = clamp(1.0 - l * 2.0, 0.0, 1.0);
|
||||
let wh = clamp(l * 2.0 - 1.0, 0.0, 1.0);
|
||||
let wm = 1.0 - ws - wh;
|
||||
var o = max(c + (P.f[0].xyz * ws + P.f[1].xyz * wm + P.f[2].xyz * wh) * 0.5, vec3<f32>(0.0));
|
||||
if (P.u[0].y != 0u) {
|
||||
let d = luminance(c) - luminance(o);
|
||||
o = max(o + d, vec3<f32>(0.0));
|
||||
}
|
||||
px = vec4<f32>(o * a, a);
|
||||
}
|
||||
}
|
||||
case 2u: {
|
||||
if (a > 0.0) {
|
||||
let c = px.xyz / a;
|
||||
let hsl = rgb_to_hsl(c);
|
||||
let vib = P.f[0].x;
|
||||
let sat = P.f[0].y;
|
||||
let s = clamp(hsl.y * (1.0 + sat) + vib * (1.0 - hsl.y) * min(hsl.y, 0.5), 0.0, 1.0);
|
||||
px = vec4<f32>(hsl_to_rgb(hsl.x, s, hsl.z) * a, a);
|
||||
}
|
||||
}
|
||||
case 3u: {
|
||||
if (a > 0.0) {
|
||||
let c = px.xyz / a;
|
||||
var w = array<f32, 6>(P.f[0].x, P.f[0].y, P.f[0].z, P.f[0].w, P.f[1].x, P.f[1].y);
|
||||
let hsl = rgb_to_hsl(c);
|
||||
let hp = hsl.x * 6.0;
|
||||
let i = u32(max(floor(hp), 0.0)) % 6u;
|
||||
let f = hp - floor(hp);
|
||||
let wt = w[i] + (w[(i + 1u) % 6u] - w[i]) * f;
|
||||
let base = luminance(c);
|
||||
let gray = max(base + (wt - 0.5) * hsl.y * 0.6, 0.0);
|
||||
var o = vec3<f32>(gray);
|
||||
if (P.u[0].y != 0u) {
|
||||
o = hsl_to_rgb(P.f[2].x, P.f[2].y, min(gray, 1.0));
|
||||
}
|
||||
px = vec4<f32>(o * a, a);
|
||||
}
|
||||
}
|
||||
case 4u: {
|
||||
if (a > 0.0) {
|
||||
let c = px.xyz / a;
|
||||
let l = clamp(luminance(c), 0.0, 1.0);
|
||||
var t: vec3<f32>;
|
||||
if (l < 0.5) {
|
||||
t = fxc_mix(P.f[2].xyz, P.f[1].xyz, l * 2.0);
|
||||
} else {
|
||||
t = fxc_mix(P.f[1].xyz, P.f[0].xyz, (l - 0.5) * 2.0);
|
||||
}
|
||||
px = vec4<f32>(fxc_mix(c, t, P.f[3].x) * a, a);
|
||||
}
|
||||
}
|
||||
case 5u: {
|
||||
if (a > 0.0) {
|
||||
let c = px.xyz / a;
|
||||
var ph = fract_euclid(fxc_colorama_phase(c, a, P.u[0].y) * P.f[0].x + P.f[0].y);
|
||||
if (P.u[0].z == 0u) {
|
||||
ph = floor(ph * 6.0) / 6.0;
|
||||
}
|
||||
let o = fxc_mix(c, fxc_hsv_to_rgb(ph, 1.0, 1.0), P.f[0].z);
|
||||
px = vec4<f32>(o * a, a);
|
||||
}
|
||||
}
|
||||
case 6u: {
|
||||
if (a > 0.0) {
|
||||
let c = px.xyz / a;
|
||||
var o: vec3<f32>;
|
||||
for (var i = 0; i < 3; i++) {
|
||||
let r = P.f[i];
|
||||
o[i] = max(c.x * r.x + c.y * r.y + c.z * r.z + r.w, 0.0);
|
||||
}
|
||||
if (P.u[0].y != 0u) {
|
||||
o = vec3<f32>(o.x);
|
||||
}
|
||||
px = vec4<f32>(o * a, a);
|
||||
}
|
||||
}
|
||||
case 7u: {
|
||||
// color2::map_ca: fully transparent black stays.
|
||||
if (a <= 0.0 && px.x == 0.0 && px.y == 0.0 && px.z == 0.0) {
|
||||
textureStore(out, p, px);
|
||||
return;
|
||||
}
|
||||
let c = fxc_unpremul(px);
|
||||
var w = fxc_sc_weights(c);
|
||||
var o = c;
|
||||
let relative = P.u[0].y != 0u;
|
||||
for (var r = 0u; r < 9u; r++) {
|
||||
let wr = w[r];
|
||||
if (wr <= 0.0) {
|
||||
continue;
|
||||
}
|
||||
let k = data[r * 4u + 3u];
|
||||
for (var i = 0u; i < 3u; i++) {
|
||||
let ink = 1.0 - c[i];
|
||||
var delta = data[r * 4u + i] + k;
|
||||
if (relative) {
|
||||
delta = delta * max(ink, 0.0);
|
||||
}
|
||||
o[i] -= wr * delta;
|
||||
}
|
||||
}
|
||||
let na = clamp(max(a, 0.0), 0.0, 1.0);
|
||||
px = vec4<f32>(max(o, vec3<f32>(0.0)) * na, na);
|
||||
}
|
||||
case 8u: {
|
||||
let c = fxc_unpremul(px);
|
||||
let key = P.f[0].xyz;
|
||||
let tol = P.f[0].w;
|
||||
let soft = P.f[1].x;
|
||||
var d: f32;
|
||||
switch P.u[0].y {
|
||||
case 1u: {
|
||||
let hsl = rgb_to_hsl(c);
|
||||
if (hsl.y < 0.02) {
|
||||
d = 1.0;
|
||||
} else {
|
||||
let dh = abs(hsl.x - P.f[1].w);
|
||||
d = min(dh, 1.0 - dh) * 2.0;
|
||||
}
|
||||
}
|
||||
case 2u: {
|
||||
let s = c.x + c.y + c.z;
|
||||
var pc = vec2<f32>(1.0 / 3.0, 1.0 / 3.0);
|
||||
if (s > 1e-6) {
|
||||
pc = vec2<f32>(c.x / s, c.y / s);
|
||||
}
|
||||
let dx = pc.x - P.f[1].y;
|
||||
let dy = pc.y - P.f[1].z;
|
||||
d = sqrt(dx * dx + dy * dy) / 1.4142135;
|
||||
}
|
||||
default: {
|
||||
let e = c - key;
|
||||
d = sqrt(e.x * e.x + e.y * e.y + e.z * e.z) / 1.7320508;
|
||||
}
|
||||
}
|
||||
var v: f32;
|
||||
if (soft > 1e-6) {
|
||||
v = clamp((d - tol) / soft, 0.0, 1.0);
|
||||
} else if (d > tol) {
|
||||
v = 1.0;
|
||||
} else {
|
||||
v = 0.0;
|
||||
}
|
||||
if (P.u[0].z != 0u) {
|
||||
v = 1.0 - v;
|
||||
}
|
||||
let k = clamp(v, 0.0, 1.0);
|
||||
if (P.u[0].w == 2u) {
|
||||
let m = px.w * k;
|
||||
px = vec4<f32>(m, m, m, 1.0);
|
||||
} else {
|
||||
px = px * k;
|
||||
}
|
||||
}
|
||||
default: {}
|
||||
}
|
||||
textureStore(out, p, px);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Lumetri Color (color3)
|
||||
|
||||
fn fxc_tone_curve(x: f32, s: f32, m: f32, h: f32) -> f32 {
|
||||
if (s == 0.0 && m == 0.0 && h == 0.0) {
|
||||
return x;
|
||||
}
|
||||
let t = clamp(x, 0.0, 1.0);
|
||||
let d0 = (t - 0.25) / 0.25;
|
||||
let d1 = (t - 0.5) / 0.25;
|
||||
let d2 = (t - 0.75) / 0.25;
|
||||
let b0 = max(1.0 - d0 * d0, 0.0);
|
||||
let b1 = max(1.0 - d1 * d1, 0.0);
|
||||
let b2 = max(1.0 - d2 * d2, 0.0);
|
||||
return x + 0.25 * (s * (b0 * b0) + m * (b1 * b1) + h * (b2 * b2));
|
||||
}
|
||||
|
||||
fn fxc_d3(i: u32) -> vec3<f32> {
|
||||
return vec3<f32>(data[i], data[i + 1u], data[i + 2u]);
|
||||
}
|
||||
|
||||
// data: 0 temperature, 1 tint, 2 exposure gain, 3 contrast, 4 highlights, 5 shadows, 6 whites,
|
||||
// 7 blacks, 8 saturation, 9 look intensity, 10 faded film, 11 vibrance, 12 creative saturation,
|
||||
// 13 tint balance pivot, 14..17 shadow tint, 17..20 highlight tint, 20..32 curves (master, red,
|
||||
// green, blue × shadows / midtones / highlights), 32..41 wheels (shadows, midtones, highlights),
|
||||
// 41 vignette amount, 42 vignette start, 43 roundness, 44 feather, 45 cx, 46 cy, 47 lw, 48 lh,
|
||||
// 49 aspect.
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxc_lumetri(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = fxc_pixel(gid);
|
||||
if (p.x < 0) {
|
||||
return;
|
||||
}
|
||||
var px = textureLoad(src, p, 0);
|
||||
let a = px.w;
|
||||
if (a <= 0.0) {
|
||||
textureStore(out, p, px);
|
||||
return;
|
||||
}
|
||||
var c = fxc_unpremul(px);
|
||||
let temp = data[0];
|
||||
let tint = data[1];
|
||||
if (temp != 0.0 || tint != 0.0) {
|
||||
c = vec3<f32>(c.x * (1.0 + 0.25 * temp), c.y * (1.0 - 0.25 * tint), c.z * (1.0 - 0.25 * temp));
|
||||
}
|
||||
let expo = data[2];
|
||||
if (expo != 1.0) {
|
||||
c = c * expo;
|
||||
}
|
||||
let contrast = data[3];
|
||||
if (contrast != 0.0) {
|
||||
c = 0.5 + (c - 0.5) * (1.0 + contrast);
|
||||
}
|
||||
let hi = data[4];
|
||||
let sh = data[5];
|
||||
let wh = data[6];
|
||||
let bl = data[7];
|
||||
if (hi != 0.0 || sh != 0.0 || wh != 0.0 || bl != 0.0) {
|
||||
let l = max(luminance(c), 0.0);
|
||||
let lt = min(l, 1.0);
|
||||
let il = 1.0 - lt;
|
||||
let d = 0.25 * hi * fxc_smoothstep(0.5, 1.0, lt) + 0.25 * sh * (1.0 - fxc_smoothstep(0.0, 0.5, lt)) + 0.2 * wh * lt * lt + 0.2 * bl * (il * il);
|
||||
c = c + d;
|
||||
}
|
||||
let l0 = luminance(c);
|
||||
let sat = data[8];
|
||||
if (sat != 1.0) {
|
||||
c = l0 + (c - l0) * sat;
|
||||
}
|
||||
let intensity = data[9];
|
||||
let faded = data[10];
|
||||
let vib = data[11];
|
||||
let csat = data[12];
|
||||
let st = fxc_d3(14u);
|
||||
let ht = fxc_d3(17u);
|
||||
let zero = vec3<f32>(0.0);
|
||||
if (intensity != 0.0 && (faded != 0.0 || vib != 0.0 || csat != 1.0 || any(st != zero) || any(ht != zero))) {
|
||||
var d = c;
|
||||
if (faded != 0.0) {
|
||||
d = d * (1.0 - 0.25 * faded) + 0.12 * faded;
|
||||
}
|
||||
let l = luminance(d);
|
||||
if (vib != 0.0) {
|
||||
let mx = max(max(d.x, d.y), d.z);
|
||||
let mn = min(min(d.x, d.y), d.z);
|
||||
var s = 0.0;
|
||||
if (mx > 1e-6) {
|
||||
s = (mx - mn) / mx;
|
||||
}
|
||||
let k = 1.0 + vib * (1.0 - clamp(s, 0.0, 1.0));
|
||||
d = l + (d - l) * k;
|
||||
}
|
||||
if (csat != 1.0) {
|
||||
d = l + (d - l) * csat;
|
||||
}
|
||||
let pivot = data[13];
|
||||
let lt = clamp(l, 0.0, 1.0);
|
||||
let wsh = 1.0 - fxc_smoothstep(0.0, pivot, lt);
|
||||
let whi = fxc_smoothstep(pivot, 1.0, lt);
|
||||
d = d + 0.5 * (st * wsh + ht * whi);
|
||||
c = c + (d - c) * intensity;
|
||||
}
|
||||
for (var i = 0u; i < 3u; i++) {
|
||||
c[i] = fxc_tone_curve(c[i], data[20], data[21], data[22]);
|
||||
}
|
||||
for (var i = 0u; i < 3u; i++) {
|
||||
let b = 23u + i * 3u;
|
||||
c[i] = fxc_tone_curve(c[i], data[b], data[b + 1u], data[b + 2u]);
|
||||
}
|
||||
let ws = fxc_d3(32u);
|
||||
let wm = fxc_d3(35u);
|
||||
let wh3 = fxc_d3(38u);
|
||||
if (any(ws != zero) || any(wm != zero) || any(wh3 != zero)) {
|
||||
let lt = clamp(luminance(c), 0.0, 1.0);
|
||||
let ka = 1.0 - fxc_smoothstep(0.0, 0.5, lt);
|
||||
let kz = fxc_smoothstep(0.5, 1.0, lt);
|
||||
let km = 1.0 - ka - kz;
|
||||
c = c + 0.5 * (ws * ka + wm * km + wh3 * kz);
|
||||
}
|
||||
let vig = data[41];
|
||||
if (vig != 0.0) {
|
||||
let x = f32(p.x) + 0.5;
|
||||
let y = f32(p.y) + 0.5;
|
||||
let lw = data[47];
|
||||
let lh = data[48];
|
||||
var nx = (x - data[45]) / max(lw * 0.5, 1e-6);
|
||||
var ny = (y - data[46]) / max(lh * 0.5, 1e-6);
|
||||
let vround = data[43];
|
||||
let aspect = data[49];
|
||||
if (vround > 0.0) {
|
||||
nx = nx * (1.0 + (max(aspect, 1.0) - 1.0) * vround);
|
||||
ny = ny * (1.0 + (max(1.0 / aspect, 1.0) - 1.0) * vround);
|
||||
}
|
||||
let pw = 2.0 + max(-vround, 0.0) * 6.0;
|
||||
let r = powz(powz(abs(nx), pw) + powz(abs(ny), pw), 1.0 / pw) / 1.4142135;
|
||||
let start = data[42];
|
||||
let f = fxc_smoothstep(start, start + 0.05 + data[44] * (1.05 - start), r);
|
||||
if (vig > 0.0) {
|
||||
c = c + (1.0 - c) * min(vig * 0.2 * f, 1.0);
|
||||
} else {
|
||||
let k = 1.0 + vig * 0.2 * f;
|
||||
c = c * max(k, 0.0);
|
||||
}
|
||||
}
|
||||
c = max(c, zero);
|
||||
textureStore(out, p, vec4<f32>(c * a, a));
|
||||
}
|
||||
|
||||
// Straight-colour luminance plane (util::Plane::luma) in x.
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxc_luma(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = fxc_pixel(gid);
|
||||
if (p.x < 0) {
|
||||
return;
|
||||
}
|
||||
let px = textureLoad(src, p, 0);
|
||||
textureStore(out, p, vec4<f32>(luminance(fxc_unpremul(px)), 0.0, 0.0, 0.0));
|
||||
}
|
||||
|
||||
// Lumetri Sharpen: `src` + (luma − blurred luma `aux`.x) × amount. f[0].x = sharpen × 2.
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxc_sharpen(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = fxc_pixel(gid);
|
||||
if (p.x < 0) {
|
||||
return;
|
||||
}
|
||||
let px = textureLoad(src, p, 0);
|
||||
let a = px.w;
|
||||
if (a <= 0.0) {
|
||||
textureStore(out, p, px);
|
||||
return;
|
||||
}
|
||||
let c = fxc_unpremul(px);
|
||||
let d = (luminance(c) - textureLoad(aux, p, 0).x) * P.f[0].x;
|
||||
textureStore(out, p, vec4<f32>(max(c + d, vec3<f32>(0.0)) * a, a));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- planes (util)
|
||||
|
||||
// One morphology pass (util::minmax_row, edges repeated) of radius u[0].x along rows (u[0].y = 0)
|
||||
// or columns; u[1] = per-channel operation: 0 min, 1 max, 2 copy.
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxc_morph(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = fxc_pixel(gid);
|
||||
if (p.x < 0) {
|
||||
return;
|
||||
}
|
||||
let dims = out_dims();
|
||||
let r = i32(P.u[0].x);
|
||||
let vertical = P.u[0].y != 0u;
|
||||
let modes = P.u[1];
|
||||
let centre = textureLoad(src, p, 0);
|
||||
var lo = centre;
|
||||
var hi = centre;
|
||||
for (var k = -r; k <= r; k++) {
|
||||
var q = p;
|
||||
if (vertical) {
|
||||
q.y = clamp(p.y + k, 0, dims.y - 1);
|
||||
} else {
|
||||
q.x = clamp(p.x + k, 0, dims.x - 1);
|
||||
}
|
||||
let v = textureLoad(src, q, 0);
|
||||
lo = min(lo, v);
|
||||
hi = max(hi, v);
|
||||
}
|
||||
var o = centre;
|
||||
for (var i = 0; i < 4; i++) {
|
||||
if (modes[i] == 0u) {
|
||||
o[i] = lo[i];
|
||||
} else if (modes[i] == 1u) {
|
||||
o[i] = hi[i];
|
||||
}
|
||||
}
|
||||
textureStore(out, p, o);
|
||||
}
|
||||
|
||||
// `src` + (`aux` − `src`) × f[0].x (Plane::zip_map lerp).
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxc_lerp(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = fxc_pixel(gid);
|
||||
if (p.x < 0) {
|
||||
return;
|
||||
}
|
||||
let x = textureLoad(src, p, 0);
|
||||
let y = textureLoad(aux, p, 0);
|
||||
textureStore(out, p, x + (y - x) * P.f[0].x);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Key Light (keylight.rs)
|
||||
|
||||
fn fxc_neutralise(c: vec3<f32>, bias: vec3<f32>) -> vec3<f32> {
|
||||
let l = max(luminance(bias), 1e-4);
|
||||
return vec3<f32>(c.x * l / max(bias.x, 1e-4), c.y * l / max(bias.y, 1e-4), c.z * l / max(bias.z, 1e-4));
|
||||
}
|
||||
|
||||
fn fxc_screen_diff(c: vec3<f32>, ix: vec3<u32>, bal: f32) -> f32 {
|
||||
return c[ix.x] - (bal * c[ix.y] + (1.0 - bal) * c[ix.z]);
|
||||
}
|
||||
|
||||
fn fxc_correct(c0: vec3<f32>, sat: f32, contrast: f32, bright: f32) -> vec3<f32> {
|
||||
let l = luminance(c0);
|
||||
let c = l + (c0 - l) * sat;
|
||||
return ((c - 0.5) * contrast + 0.5) * bright;
|
||||
}
|
||||
|
||||
// Raw screen matte of `src` (the key source): out = (clipped, clipped, raw, 0).
|
||||
// u[0].xyz = primary, other channels; f[0] = (alpha bias, screen difference);
|
||||
// f[1] = (gain, balance, clip black, clip white).
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxc_kl_raw(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = fxc_pixel(gid);
|
||||
if (p.x < 0) {
|
||||
return;
|
||||
}
|
||||
let c = fxc_unpremul(textureLoad(src, p, 0));
|
||||
let d = fxc_screen_diff(fxc_neutralise(c, P.f[0].xyz), P.u[0].xyz, P.f[1].y);
|
||||
let raw = clamp(1.0 - P.f[1].x * d / P.f[0].w, 0.0, 1.0);
|
||||
let cb = P.f[1].z;
|
||||
let m = clamp((raw - cb) / max(P.f[1].w - cb, 1e-4), 0.0, 1.0);
|
||||
textureStore(out, p, vec4<f32>(m, m, raw, 0.0));
|
||||
}
|
||||
|
||||
// Clip Rollback: `src` = (shrunk, grown) clipped matte, `aux` = raw planes (clipped, _, raw).
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxc_kl_rollback(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = fxc_pixel(gid);
|
||||
if (p.x < 0) {
|
||||
return;
|
||||
}
|
||||
let sg = textureLoad(src, p, 0);
|
||||
let r = textureLoad(aux, p, 0);
|
||||
var m = r.x;
|
||||
if (sg.y > sg.x + 1e-4) {
|
||||
m = min(max(m, r.z), sg.y);
|
||||
}
|
||||
textureStore(out, p, vec4<f32>(m, m, r.z, 0.0));
|
||||
}
|
||||
|
||||
// Planes for the final pass: (screen matte from `src`.x, raw from `aux`.z, edge 0, 0).
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxc_kl_pack(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = fxc_pixel(gid);
|
||||
if (p.x < 0) {
|
||||
return;
|
||||
}
|
||||
textureStore(out, p, vec4<f32>(textureLoad(src, p, 0).x, textureLoad(aux, p, 0).z, 0.0, 0.0));
|
||||
}
|
||||
|
||||
// Combined matte from the screen matte and the source alpha (no inside / outside masks).
|
||||
fn fxc_kl_combined(sm: f32, src_a: f32, mode: u32) -> f32 {
|
||||
var a = sm;
|
||||
let sa = clamp(src_a, 0.0, 1.0);
|
||||
var ins = 0.0;
|
||||
if (mode == 1u) {
|
||||
ins = max(ins, sa);
|
||||
}
|
||||
a = max(a, ins);
|
||||
if (mode == 2u) {
|
||||
a = a * sa;
|
||||
}
|
||||
return clamp(a, 0.0, 1.0);
|
||||
}
|
||||
|
||||
// Edge band of the combined matte: `src` = source, `aux` = planes; u[0].x = source alpha mode.
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxc_kl_band(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = fxc_pixel(gid);
|
||||
if (p.x < 0) {
|
||||
return;
|
||||
}
|
||||
let a = fxc_kl_combined(textureLoad(aux, p, 0).x, textureLoad(src, p, 0).w, P.u[0].x);
|
||||
textureStore(out, p, vec4<f32>(select(0.0, 1.0, a > 1e-3 && a < 0.999), 0.0, 0.0, 0.0));
|
||||
}
|
||||
|
||||
// Planes `src` with the edge weight from the processed band `aux`.x; f[0].x = hardness.
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxc_kl_edge(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = fxc_pixel(gid);
|
||||
if (p.x < 0) {
|
||||
return;
|
||||
}
|
||||
let pl = textureLoad(src, p, 0);
|
||||
let e = min(textureLoad(aux, p, 0).x * (1.0 + P.f[0].x * 4.0), 1.0);
|
||||
textureStore(out, p, vec4<f32>(pl.x, pl.y, e, 0.0));
|
||||
}
|
||||
|
||||
fn fxc_kl_replace(fg: vec3<f32>, c: vec3<f32>, method: u32, rc: vec3<f32>, amt: f32) -> vec3<f32> {
|
||||
if (amt <= 0.0) {
|
||||
return fg;
|
||||
}
|
||||
switch method {
|
||||
case 1u: { return fg + (c - fg) * amt; }
|
||||
case 2u: { return rc; }
|
||||
case 3u: {
|
||||
let l = luminance(c) / max(luminance(rc), 1e-4);
|
||||
return fg + (rc * l - fg) * amt;
|
||||
}
|
||||
default: { return fg; }
|
||||
}
|
||||
}
|
||||
|
||||
// Final pass: `src` = source, `aux` = planes (screen matte, raw, edge).
|
||||
// u[0] = (primary, others, view); u[1] = (replace method, foreground cc, unpremultiply,
|
||||
// source alpha mode); f[0] = (screen, balance); f[1] = (despill bias, l0); f[2] = replace colour;
|
||||
// f[3] = fg cc (sat, contrast, bright); f[4] = edge cc; f[5] = crops (l, r, t, b);
|
||||
// f[6] = (offset, scale, _); f[7] = (layer w, layer h).
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxc_kl_final(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = fxc_pixel(gid);
|
||||
if (p.x < 0) {
|
||||
return;
|
||||
}
|
||||
let s = textureLoad(src, p, 0);
|
||||
let pl = textureLoad(aux, p, 0);
|
||||
let ix = P.u[0].xyz;
|
||||
let view = P.u[0].w;
|
||||
let c = fxc_unpremul(s);
|
||||
let sm = pl.x;
|
||||
let raw = pl.y;
|
||||
let edge = pl.z;
|
||||
let a = fxc_kl_combined(sm, s.w, P.u[1].w);
|
||||
let screen = P.f[0].xyz;
|
||||
let bal = P.f[0].w;
|
||||
let dbias = P.f[1].xyz;
|
||||
let fg_cc = P.u[1].y != 0u;
|
||||
let ar = max(raw, 1e-4);
|
||||
var fg = max((c - (1.0 - ar) * screen) / ar, vec3<f32>(0.0));
|
||||
let spill = max(fxc_screen_diff(fxc_neutralise(fg, dbias), ix, bal), 0.0);
|
||||
fg[ix.x] -= spill / P.f[1].w;
|
||||
let intermediate = fg;
|
||||
fg = fxc_kl_replace(fg, c, P.u[1].x, P.f[2].xyz, max(sm - raw, 0.0));
|
||||
if (fg_cc) {
|
||||
fg = fxc_correct(fg, P.f[3].x, P.f[3].y, P.f[3].z);
|
||||
}
|
||||
if (edge > 0.0) {
|
||||
let ce = fxc_correct(fg, P.f[4].x, P.f[4].y, P.f[4].z);
|
||||
fg = fg + (ce - fg) * edge;
|
||||
}
|
||||
var o: vec4<f32>;
|
||||
switch view {
|
||||
case 1u: { o = vec4<f32>(s.w, s.w, s.w, 1.0); }
|
||||
case 2u: {
|
||||
var cs = fxc_neutralise(c, dbias);
|
||||
let s2 = max(fxc_screen_diff(cs, ix, bal), 0.0);
|
||||
cs[ix.x] -= s2;
|
||||
if (fg_cc) {
|
||||
cs = fxc_correct(cs, P.f[3].x, P.f[3].y, P.f[3].z);
|
||||
}
|
||||
o = vec4<f32>(cs, 1.0);
|
||||
}
|
||||
case 3u: { o = vec4<f32>(edge, edge, edge, 1.0); }
|
||||
case 4u: { o = vec4<f32>(sm, sm, sm, 1.0); }
|
||||
case 5u, 6u: { o = vec4<f32>(0.0, 0.0, 0.0, 1.0); }
|
||||
case 7u: { o = vec4<f32>(a, a, a, 1.0); }
|
||||
case 8u: {
|
||||
var v = 0.5;
|
||||
if (a <= 1e-3) {
|
||||
v = 0.0;
|
||||
} else if (a >= 0.999) {
|
||||
v = 1.0;
|
||||
}
|
||||
o = vec4<f32>(v, v, v, 1.0);
|
||||
}
|
||||
case 9u: { o = vec4<f32>(intermediate * sm, sm); }
|
||||
default: {
|
||||
let x = f32(p.x) + 0.5;
|
||||
let y = f32(p.y) + 0.5;
|
||||
let lx = (x - P.f[6].x) / P.f[6].z / P.f[7].x;
|
||||
let ly = (y - P.f[6].y) / P.f[6].z / P.f[7].y;
|
||||
let cr = P.f[5];
|
||||
let cropped = lx < cr.x || lx > 1.0 - cr.y || ly < cr.z || ly > 1.0 - cr.w;
|
||||
if (cropped) {
|
||||
o = vec4<f32>(0.0);
|
||||
} else if (P.u[1].z != 0u) {
|
||||
o = vec4<f32>(fg * a, a);
|
||||
} else {
|
||||
o = vec4<f32>(fg * a * a, a);
|
||||
}
|
||||
}
|
||||
}
|
||||
textureStore(out, p, o);
|
||||
}
|
||||
|
|
@ -0,0 +1,554 @@
|
|||
// GPU effects (distort family): see src/fx_distort.rs. Each entry point mirrors the CPU effect in
|
||||
// effectcraft-effects operation for operation; parameter layouts are documented per entry.
|
||||
// Coordinates are pixel centres (x + 0.5, y + 0.5) in buffer pixels, as in util::remap.
|
||||
|
||||
const DST_TAU: f32 = 6.2831855;
|
||||
const DST_HALF_PI: f32 = 1.5707964;
|
||||
|
||||
fn dst_centre(p: vec2<i32>) -> vec2<f32> {
|
||||
return vec2<f32>(f32(p.x) + 0.5, f32(p.y) + 0.5);
|
||||
}
|
||||
|
||||
fn dst_inside(p: vec2<i32>) -> bool {
|
||||
let d = out_dims();
|
||||
return p.x < d.x && p.y < d.y;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Twirl (distort::twirl)
|
||||
// f[0] = (cx, cy, radius, angle in radians)
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn dst_twirl(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!dst_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let q = dst_centre(p);
|
||||
let c = P.f[0].xy;
|
||||
let r = P.f[0].z;
|
||||
let dx = q.x - c.x;
|
||||
let dy = q.y - c.y;
|
||||
let d = sqrt(dx * dx + dy * dy);
|
||||
var s = q;
|
||||
if (d < r) {
|
||||
let t = 1.0 - d / r;
|
||||
let a = -P.f[0].w * t * t;
|
||||
let sn = sin(a);
|
||||
let co = cos(a);
|
||||
s = vec2<f32>(c.x + dx * co - dy * sn, c.y + dx * sn + dy * co);
|
||||
}
|
||||
textureStore(out, p, sample_bilinear(src, s.x, s.y));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Bulge (distort::bulge)
|
||||
// f[0] = (cx, cy, rx, ry); f[1] = (height, taper, pin, edge); f[2] = layer rect (x, y, w, h)
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn dst_bulge(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!dst_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let q = dst_centre(p);
|
||||
let c = P.f[0].xy;
|
||||
let rx = P.f[0].z;
|
||||
let ry = P.f[0].w;
|
||||
let dx = (q.x - c.x) / rx;
|
||||
let dy = (q.y - c.y) / ry;
|
||||
let d2 = dx * dx + dy * dy;
|
||||
var s = q;
|
||||
if (d2 < 1.0) {
|
||||
var amt = P.f[1].x * pow(1.0 - d2, P.f[1].y) * 0.5;
|
||||
if (P.f[1].z > 0.5) {
|
||||
let l = P.f[2];
|
||||
let edge = P.f[1].w;
|
||||
let ex = clamp(min(q.x - l.x - 0.5, l.x + l.z - 0.5 - q.x) / edge, 0.0, 1.0);
|
||||
let ey = clamp(min(q.y - l.y - 0.5, l.y + l.w - 0.5 - q.y) / edge, 0.0, 1.0);
|
||||
amt *= min(ex, ey);
|
||||
}
|
||||
let k = 1.0 - amt;
|
||||
s = vec2<f32>(c.x + dx * rx * k, c.y + dy * ry * k);
|
||||
}
|
||||
textureStore(out, p, sample_bilinear(src, s.x, s.y));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Wave Warp (distort::wave_warp)
|
||||
|
||||
fn dst_half_circle(u: f32) -> f32 {
|
||||
let v = 2.0 * u - 1.0;
|
||||
return sqrt(max(1.0 - v * v, 0.0));
|
||||
}
|
||||
|
||||
fn dst_wave_noise(i: f32, seed: u32) -> f32 {
|
||||
return hash_noise(bitcast<u32>(i32(i)), 0x5eedu, seed) * 2.0 - 1.0;
|
||||
}
|
||||
|
||||
// distort::wave_shape at the phase `cf + ci` in cycles (t / 2π), split into a fraction cf in
|
||||
// [0, 2) and whole cycles ci so the fraction keeps full precision.
|
||||
fn dst_wave_shape(kind: u32, cf: f32, ci: f32, seed: u32) -> f32 {
|
||||
let ph = cf - floor(cf);
|
||||
switch kind {
|
||||
case 1u: {
|
||||
return select(-1.0, 1.0, ph < 0.5);
|
||||
}
|
||||
case 2u: {
|
||||
return 1.0 - 4.0 * abs(ph - 0.5);
|
||||
}
|
||||
case 3u: {
|
||||
return 2.0 * ph - 1.0;
|
||||
}
|
||||
case 4u: {
|
||||
if (ph < 0.5) {
|
||||
return dst_half_circle(ph * 2.0);
|
||||
}
|
||||
return -dst_half_circle(ph * 2.0 - 1.0);
|
||||
}
|
||||
case 5u: {
|
||||
return 2.0 * dst_half_circle(fract(ph * 2.0)) - 1.0;
|
||||
}
|
||||
case 6u: {
|
||||
return 1.0 - 2.0 * dst_half_circle(fract(ph * 2.0));
|
||||
}
|
||||
case 7u: {
|
||||
return dst_wave_noise(floor(cf * 2.0) + 2.0 * ci, seed);
|
||||
}
|
||||
case 8u: {
|
||||
let u = cf * 2.0;
|
||||
let i = floor(u);
|
||||
let f = u - i;
|
||||
let s = f * f * (3.0 - 2.0 * f);
|
||||
let n = i + 2.0 * ci;
|
||||
return dst_wave_noise(n, seed) * (1.0 - s) + dst_wave_noise(n + 1.0, seed) * s;
|
||||
}
|
||||
default: {
|
||||
return sin(ph * DST_TAU);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// distort::wave_pin.
|
||||
fn dst_wave_pin(pin: u32, x: f32, y: f32, r: vec4<f32>) -> f32 {
|
||||
let u = clamp((x - r.x - 0.5) / max(r.z - 1.0, 1.0), 0.0, 1.0);
|
||||
let v = clamp((y - r.y - 0.5) / max(r.w - 1.0, 1.0), 0.0, 1.0);
|
||||
let horiz = clamp(min(v, 1.0 - v) * 2.0, 0.0, 1.0);
|
||||
let vert = clamp(min(u, 1.0 - u) * 2.0, 0.0, 1.0);
|
||||
switch pin {
|
||||
case 1u: { return min(horiz, vert); }
|
||||
case 2u: { return min(sqrt((u - 0.5) * (u - 0.5) + (v - 0.5) * (v - 0.5)) * 2.0, 1.0); }
|
||||
case 3u: { return u; }
|
||||
case 4u: { return v; }
|
||||
case 5u: { return 1.0 - u; }
|
||||
case 6u: { return 1.0 - v; }
|
||||
case 7u: { return horiz; }
|
||||
case 8u: { return vert; }
|
||||
default: { return 1.0; }
|
||||
}
|
||||
}
|
||||
|
||||
// u[0] = (wave type, pinning, seed, buffer width); f[0] = (dx, dy, height); f[2] = layer rect
|
||||
// (x, y, w, h). data: the phase in cycles (along / width + phase / 2π) as per-column and per-row
|
||||
// (fraction, whole cycles) pairs: fx (w), ix (w), fy (h), iy (h).
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn dst_wavewarp(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!dst_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let q = dst_centre(p);
|
||||
let dx = P.f[0].x;
|
||||
let dy = P.f[0].y;
|
||||
let w = P.u[0].w;
|
||||
let x = u32(p.x);
|
||||
let y = u32(p.y);
|
||||
let cf = data[x] + data[2u * w + y];
|
||||
let ci = data[w + x] + data[2u * w + u32(out_dims().y) + y];
|
||||
let disp = dst_wave_shape(P.u[0].x, cf, ci, P.u[0].z) * P.f[0].z * dst_wave_pin(P.u[0].y, q.x, q.y, P.f[2]);
|
||||
textureStore(out, p, sample_bilinear(src, q.x + dy * disp, q.y - dx * disp));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Ripple (distort::ripple)
|
||||
// u[0].x = asymmetric; f[0] = (cx, cy, radius, wave width); f[1] = (wave height, phase mod 2π)
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn dst_ripple(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!dst_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let q = dst_centre(p);
|
||||
let c = P.f[0].xy;
|
||||
let r = P.f[0].z;
|
||||
let dx = q.x - c.x;
|
||||
let dy = q.y - c.y;
|
||||
let d = sqrt(dx * dx + dy * dy);
|
||||
var s = q;
|
||||
if (d < r && d != 0.0) {
|
||||
let h = P.f[1].x;
|
||||
let fall = 1.0 - d / r;
|
||||
let arg = d / P.f[0].w * DST_TAU + P.f[1].y;
|
||||
let o = sin(arg) * h * fall;
|
||||
var t = 0.0;
|
||||
if (P.u[0].x != 0u) {
|
||||
t = cos(arg) * h * fall * 0.5;
|
||||
}
|
||||
let ux = dx / d;
|
||||
let uy = dy / d;
|
||||
s = vec2<f32>(q.x + ux * o - uy * t, q.y + uy * o + ux * t);
|
||||
}
|
||||
textureStore(out, p, sample_bilinear(src, s.x, s.y));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Lens (distort2::cc_lens)
|
||||
// f[0] = (cx, cy, radius, convergence exponent)
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn dst_cclens(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!dst_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let q = dst_centre(p);
|
||||
let c = P.f[0].xy;
|
||||
let r = P.f[0].z;
|
||||
let dx = q.x - c.x;
|
||||
let dy = q.y - c.y;
|
||||
let d = sqrt(dx * dx + dy * dy);
|
||||
if (d >= r) {
|
||||
textureStore(out, p, vec4<f32>(0.0));
|
||||
return;
|
||||
}
|
||||
var s = q;
|
||||
if (d >= 1e-9) {
|
||||
let g = powz(asin(min(d / r, 1.0)) / DST_HALF_PI, P.f[0].w);
|
||||
let k = r * g / d;
|
||||
s = vec2<f32>(c.x + dx * k, c.y + dy * k);
|
||||
}
|
||||
textureStore(out, p, sample_bilinear(src, s.x, s.y));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Turbulent Displace (distort2)
|
||||
|
||||
// distort2::fbm. u[0].w = octave count; f[1].w = fraction of the last octave; f[3].y = falloff.
|
||||
fn dst_fbm(u: f32, v: f32, z: f32, seed: u32) -> f32 {
|
||||
let n = P.u[0].w;
|
||||
let frac = P.f[1].w;
|
||||
let falloff = P.f[3].y;
|
||||
var sum = 0.0;
|
||||
var norm = 0.0;
|
||||
var amp = 1.0;
|
||||
var f = 1.0;
|
||||
for (var o = 0u; o < n; o++) {
|
||||
var w = 1.0;
|
||||
if (o + 1u == n && frac > 0.0) {
|
||||
w = frac;
|
||||
}
|
||||
sum += value_noise(u * f, v * f, z + f32(o) * 5.17, seed + o * 101u) * amp * w;
|
||||
norm += amp * w;
|
||||
amp *= falloff;
|
||||
f *= 2.0;
|
||||
}
|
||||
return sum / max(norm, 1e-6);
|
||||
}
|
||||
|
||||
// The centred noise `n(u, v, s)` (cross-faded over the evolution cycle when u[1].x is set).
|
||||
// f[1] = (z, z - cycle, cycle position t).
|
||||
fn dst_turb_n(u: f32, v: f32, s: u32) -> f32 {
|
||||
let seed = P.u[0].z + s;
|
||||
if (P.u[1].x != 0u) {
|
||||
let t = P.f[1].z;
|
||||
return dst_fbm(u, v, P.f[1].x, seed) * (1.0 - t) + dst_fbm(u, v, P.f[1].y, seed) * t - 0.5;
|
||||
}
|
||||
return dst_fbm(u, v, P.f[1].x, seed) - 0.5;
|
||||
}
|
||||
|
||||
// u[0] = (displacement kind, pinning, seed, octaves); u[1].x = cycle evolution;
|
||||
// f[0] = (offset x, offset y, size, amount); f[1] = (z0, z1, t, last-octave fraction);
|
||||
// f[2] = layer rect (x, y, w, h); f[3] = (edge, falloff)
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn dst_turbulent(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!dst_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let q = dst_centre(p);
|
||||
let kind = P.u[0].x;
|
||||
let amount = P.f[0].w;
|
||||
let u = (q.x - P.f[0].x) / P.f[0].z;
|
||||
let v = (q.y - P.f[0].y) / P.f[0].z;
|
||||
var d = vec2<f32>(0.0);
|
||||
switch kind {
|
||||
case 1u, 2u, 4u, 5u: {
|
||||
let e = 0.05;
|
||||
let gx = (dst_turb_n(u + e, v, 0u) - dst_turb_n(u - e, v, 0u)) / (2.0 * e);
|
||||
let gy = (dst_turb_n(u, v + e, 0u) - dst_turb_n(u, v - e, 0u)) / (2.0 * e);
|
||||
let k = amount * 0.25;
|
||||
if (kind == 1u || kind == 4u) {
|
||||
d = vec2<f32>(gx * k, gy * k);
|
||||
} else {
|
||||
d = vec2<f32>(-gy * k, gx * k);
|
||||
}
|
||||
}
|
||||
case 6u: {
|
||||
d = vec2<f32>(0.0, dst_turb_n(u, v, 17u) * amount);
|
||||
}
|
||||
case 7u: {
|
||||
d = vec2<f32>(dst_turb_n(u, v, 0u) * amount, 0.0);
|
||||
}
|
||||
case 8u: {
|
||||
let a = dst_turb_n(u, v, 0u) * amount;
|
||||
d = vec2<f32>(a, a);
|
||||
}
|
||||
default: {
|
||||
d = vec2<f32>(dst_turb_n(u, v, 0u) * amount, dst_turb_n(u, v, 17u) * amount);
|
||||
}
|
||||
}
|
||||
let r = P.f[2];
|
||||
let edge = P.f[3].x;
|
||||
let l = clamp((q.x - r.x - 0.5) / edge, 0.0, 1.0);
|
||||
let rr = clamp((r.x + r.z - 0.5 - q.x) / edge, 0.0, 1.0);
|
||||
let t = clamp((q.y - r.y - 0.5) / edge, 0.0, 1.0);
|
||||
let bt = clamp((r.y + r.w - 0.5 - q.y) / edge, 0.0, 1.0);
|
||||
var k = 1.0;
|
||||
switch P.u[0].y {
|
||||
case 1u: { k = min(min(l, rr), min(t, bt)); }
|
||||
case 2u: { k = min(t, bt); }
|
||||
case 3u: { k = min(l, rr); }
|
||||
case 4u: { k = l; }
|
||||
case 5u: { k = t; }
|
||||
case 6u: { k = rr; }
|
||||
case 7u: { k = bt; }
|
||||
default: {}
|
||||
}
|
||||
textureStore(out, p, sample_bilinear(src, q.x + d.x * k, q.y + d.y * k));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Displacement Map (distort2)
|
||||
|
||||
// util::pick for a straight colour (source index in util::SRC_ORDER).
|
||||
fn dst_pick(s: u32, c: vec3<f32>, a: f32) -> f32 {
|
||||
switch s {
|
||||
case 0u: { return c.x; }
|
||||
case 1u: { return c.y; }
|
||||
case 2u: { return c.z; }
|
||||
case 3u: { return a; }
|
||||
case 4u: { return luminance(c); }
|
||||
case 5u: { return rgb_to_hsl(c).x; }
|
||||
case 6u: { return rgb_to_hsl(c).z; }
|
||||
case 7u: { return rgb_to_hsl(c).y; }
|
||||
case 8u: { return 1.0; }
|
||||
case 9u: { return 0.5; }
|
||||
default: { return 0.0; }
|
||||
}
|
||||
}
|
||||
|
||||
// u[0] = (horizontal source, vertical source, map mode: 0 self, 1 fitted layer (transparent
|
||||
// outside), 2 tiled layer (edges repeat), flags: 1 wrap, 2 Center Map bounds);
|
||||
// u[1] = (buffer width, buffer height); f[0] = (max horizontal, max vertical).
|
||||
// data: per-column map x, per-row map y (the map layer's buffer pixels), per-column and
|
||||
// per-row "inside the centred map" flags. aux = the map layer.
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn dst_displace(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!dst_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let w = P.u[1].x;
|
||||
let h = P.u[1].y;
|
||||
let x = u32(p.x);
|
||||
let y = u32(p.y);
|
||||
let px = textureLoad(src, p, 0);
|
||||
let flags = P.u[0].w;
|
||||
if ((flags & 2u) != 0u && (data[w + h + x] < 0.5 || data[2u * w + h + y] < 0.5)) {
|
||||
textureStore(out, p, px);
|
||||
return;
|
||||
}
|
||||
var m = px;
|
||||
let mode = P.u[0].z;
|
||||
if (mode == 1u) {
|
||||
m = sample_bilinear(aux, data[x], data[w + y]);
|
||||
} else if (mode == 2u) {
|
||||
m = sample_bilinear_clamped(aux, data[x], data[w + y]);
|
||||
}
|
||||
var c = vec3<f32>(0.0);
|
||||
let a = m.w;
|
||||
if (a > 1e-6) {
|
||||
c = m.xyz / a;
|
||||
}
|
||||
let al = max(a, 0.0);
|
||||
var dx = 0.0;
|
||||
var dy = 0.0;
|
||||
if (P.u[0].x != 10u) {
|
||||
dx = (dst_pick(P.u[0].x, c, al) - 0.5) * 2.0 * P.f[0].x;
|
||||
}
|
||||
if (P.u[0].y != 10u) {
|
||||
dy = (dst_pick(P.u[0].y, c, al) - 0.5) * 2.0 * P.f[0].y;
|
||||
}
|
||||
if (dx == 0.0 && dy == 0.0) {
|
||||
textureStore(out, p, px);
|
||||
return;
|
||||
}
|
||||
var sx = f32(p.x) + 0.5 + dx;
|
||||
var sy = f32(p.y) + 0.5 + dy;
|
||||
if ((flags & 1u) != 0u) {
|
||||
let fw = f32(w);
|
||||
let fh = f32(h);
|
||||
sx = sx - floor(sx / fw) * fw;
|
||||
sy = sy - floor(sy / fh) * fh;
|
||||
textureStore(out, p, sample_bilinear_clamped(src, sx, sy));
|
||||
} else {
|
||||
textureStore(out, p, sample_bilinear(src, sx, sy));
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Mesh Warp (distort2::grid_warp)
|
||||
// u[0] = (row starts offset, list offset). data: 12 floats per triangle (the barycentric
|
||||
// coefficients k11 k12 k21 k22 relative to vertex c, c, then the source points a, b, c), then
|
||||
// per output row the start of its triangle list (height + 1 entries), then the lists (triangle
|
||||
// indices in the CPU's bucket order).
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn dst_mesh(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!dst_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let q = dst_centre(p);
|
||||
let rs = P.u[0].x;
|
||||
let lo = P.u[0].y;
|
||||
let i0 = u32(data[rs + u32(p.y)]);
|
||||
let i1 = u32(data[rs + u32(p.y) + 1u]);
|
||||
var cur = vec4<f32>(0.0);
|
||||
for (var i = i0; i < i1; i++) {
|
||||
if (cur.w > 0.0) {
|
||||
break;
|
||||
}
|
||||
let b = u32(data[lo + i]) * 12u;
|
||||
let ex = q.x - data[b + 4u];
|
||||
let ey = q.y - data[b + 5u];
|
||||
let l1 = data[b] * ex + data[b + 1u] * ey;
|
||||
let l2 = data[b + 2u] * ex + data[b + 3u] * ey;
|
||||
let l3 = 1.0 - l1 - l2;
|
||||
// The CPU tests against -1e-9 in f64; f32 rounding needs a wider margin so pixels on
|
||||
// shared edges are not dropped by both triangles.
|
||||
let e = -1e-5;
|
||||
if (l1 < e || l2 < e || l3 < e) {
|
||||
continue;
|
||||
}
|
||||
let sx = data[b + 6u] * l1 + data[b + 8u] * l2 + data[b + 10u] * l3;
|
||||
let sy = data[b + 7u] * l1 + data[b + 9u] * l2 + data[b + 11u] * l3;
|
||||
cur = sample_bilinear(src, sx, sy);
|
||||
}
|
||||
textureStore(out, p, cur);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Mosaic (misc::mosaic)
|
||||
// data: column tile index (w), row tile index (h), tile column bounds (nx + 1), tile row bounds
|
||||
// (ny + 1); u[0] = (w, h, nx, ny).
|
||||
|
||||
// Row sums of each tile's columns: out is nx × h.
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn dst_mosaic_rows(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!dst_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let w = P.u[0].x;
|
||||
let h = P.u[0].y;
|
||||
let xb = w + h + u32(p.x);
|
||||
var s = vec4<f32>(0.0);
|
||||
for (var x = u32(data[xb]); x < u32(data[xb + 1u]); x++) {
|
||||
s += textureLoad(src, vec2<i32>(i32(x), p.y), 0);
|
||||
}
|
||||
textureStore(out, p, s);
|
||||
}
|
||||
|
||||
// Tile averages: out is nx × ny (src = the row sums).
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn dst_mosaic_tiles(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!dst_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let w = P.u[0].x;
|
||||
let h = P.u[0].y;
|
||||
let xb = w + h + u32(p.x);
|
||||
let yb = w + h + P.u[0].z + 1u + u32(p.y);
|
||||
let y0 = u32(data[yb]);
|
||||
let y1 = u32(data[yb + 1u]);
|
||||
var s = vec4<f32>(0.0);
|
||||
for (var y = y0; y < y1; y++) {
|
||||
s += textureLoad(src, vec2<i32>(p.x, i32(y)), 0);
|
||||
}
|
||||
let n = f32((u32(data[xb + 1u]) - u32(data[xb])) * (y1 - y0));
|
||||
textureStore(out, p, s / max(n, 1.0));
|
||||
}
|
||||
|
||||
// Each pixel takes its tile's average (src = the tile averages).
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn dst_mosaic_out(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!dst_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let tx = i32(data[u32(p.x)]);
|
||||
let ty = i32(data[P.u[0].x + u32(p.y)]);
|
||||
textureStore(out, p, textureLoad(src, vec2<i32>(tx, ty), 0));
|
||||
}
|
||||
|
||||
// Sharp Colors: data = per-column / per-row source pixel; u[0].x = w.
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn dst_mosaic_sharp(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!dst_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let sx = i32(data[u32(p.x)]);
|
||||
let sy = i32(data[P.u[0].x + u32(p.y)]);
|
||||
textureStore(out, p, tex_get_clamped(src, sx, sy));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Find Edges / Emboss (misc)
|
||||
|
||||
fn dst_blend_with(orig: vec4<f32>, fx: vec4<f32>, k: f32) -> vec4<f32> {
|
||||
return fx + (orig - fx) * k;
|
||||
}
|
||||
|
||||
fn dst_lum_at(x: i32, y: i32) -> f32 {
|
||||
return luminance(tex_get_clamped(src, x, y).xyz);
|
||||
}
|
||||
|
||||
// u[0].x = invert; f[0].x = blend with original.
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn dst_findedges(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!dst_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let x = p.x;
|
||||
let y = p.y;
|
||||
let gx = dst_lum_at(x + 1, y - 1) + 2.0 * dst_lum_at(x + 1, y) + dst_lum_at(x + 1, y + 1) - dst_lum_at(x - 1, y - 1) - 2.0 * dst_lum_at(x - 1, y)
|
||||
- dst_lum_at(x - 1, y + 1);
|
||||
let gy = dst_lum_at(x - 1, y + 1) + 2.0 * dst_lum_at(x, y + 1) + dst_lum_at(x + 1, y + 1) - dst_lum_at(x - 1, y - 1) - 2.0 * dst_lum_at(x, y - 1)
|
||||
- dst_lum_at(x + 1, y - 1);
|
||||
let e = min(sqrt(gx * gx + gy * gy), 1.0);
|
||||
var v = 1.0 - e;
|
||||
if (P.u[0].x != 0u) {
|
||||
v = e;
|
||||
}
|
||||
let px = textureLoad(src, p, 0);
|
||||
let a = px.w;
|
||||
textureStore(out, p, dst_blend_with(px, vec4<f32>(v * a, v * a, v * a, a), P.f[0].x));
|
||||
}
|
||||
|
||||
// f[0] = (dx, dy, contrast, blend with original).
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn dst_emboss(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!dst_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let q = dst_centre(p);
|
||||
let d = P.f[0].xy;
|
||||
let a = sample_bilinear_clamped(src, q.x + d.x, q.y + d.y);
|
||||
let c = sample_bilinear_clamped(src, q.x - d.x, q.y - d.y);
|
||||
let l = luminance(a.xyz) - luminance(c.xyz);
|
||||
let v = clamp(0.5 + l * P.f[0].z, 0.0, 1.0);
|
||||
let px = textureLoad(src, p, 0);
|
||||
let al = px.w;
|
||||
textureStore(out, p, dst_blend_with(px, vec4<f32>(v * al, v * al, v * al, al), P.f[0].w));
|
||||
}
|
||||
|
|
@ -0,0 +1,631 @@
|
|||
// GPU effects (generate family): see src/fx_generate.rs. Each entry point mirrors the CPU effect in
|
||||
// effectcraft-effects operation for operation; parameter layouts are documented per entry.
|
||||
//
|
||||
// Hard edges (wipes) take per-column and per-row terms computed in f64 on the CPU and uploaded
|
||||
// as (hi, lo) f32 pairs in `data`: near an edge the hi parts cancel exactly, so the edge sits
|
||||
// where the CPU's f64 arithmetic puts it.
|
||||
|
||||
const GEN_DEG: f32 = 57.29577951308232;
|
||||
|
||||
// util::unpremul: (straight colour, alpha).
|
||||
fn gen_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));
|
||||
}
|
||||
|
||||
// util::smoothstep.
|
||||
fn gen_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);
|
||||
}
|
||||
|
||||
// generate::gen_blend: mode = index into BlendMode::ALL, 255 = None (replace).
|
||||
fn gen_blend(o: vec4<f32>, g: vec4<f32>, mode: u32) -> vec4<f32> {
|
||||
if (mode == 255u) {
|
||||
return g;
|
||||
}
|
||||
return blend_pixel(mode, o, g, 0.5);
|
||||
}
|
||||
|
||||
// A (hi, lo) pair from `data`.
|
||||
fn gen_hl(i: u32) -> vec2<f32> {
|
||||
return vec2<f32>(data[i], data[i + 1u]);
|
||||
}
|
||||
|
||||
// (a + b) for (hi, lo) pairs whose hi parts cancel near zero.
|
||||
fn gen_sum(a: vec2<f32>, b: vec2<f32>) -> f32 {
|
||||
return (a.x + b.x) + (a.y + b.y);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Radial Blur (raster::radial_blur)
|
||||
|
||||
// u[0] = (zoom, samples); f[0] = (cx, cy); data = per sample (sin, cos) (spin) or k (zoom).
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn gen_radial_blur(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let dims = out_dims();
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (p.x >= dims.x || p.y >= dims.y) {
|
||||
return;
|
||||
}
|
||||
let zoom = P.u[0].x != 0u;
|
||||
let n = P.u[0].y;
|
||||
let c = P.f[0].xy;
|
||||
let v = vec2<f32>(f32(p.x) + 0.5 - c.x, f32(p.y) + 0.5 - c.y);
|
||||
var acc = vec4<f32>(0.0);
|
||||
for (var i = 0u; i < n; i++) {
|
||||
var s: vec2<f32>;
|
||||
if (zoom) {
|
||||
let k = data[i];
|
||||
s = vec2<f32>(c.x + v.x * k, c.y + v.y * k);
|
||||
} else {
|
||||
let sn = data[2u * i];
|
||||
let cs = data[2u * i + 1u];
|
||||
s = vec2<f32>(c.x + v.x * cs - v.y * sn, c.y + v.x * sn + v.y * cs);
|
||||
}
|
||||
acc += sample_bilinear(src, s.x, s.y);
|
||||
}
|
||||
textureStore(out, p, acc / f32(n));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Radial Fast Blur
|
||||
|
||||
// u[0].x = zoom mode (0 standard, 1 brightest, 2 darkest); f[0] = (cx, cy, s).
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn gen_ccradialfast(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let dims = out_dims();
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (p.x >= dims.x || p.y >= dims.y) {
|
||||
return;
|
||||
}
|
||||
let mode = P.u[0].x;
|
||||
let c = P.f[0].xy;
|
||||
let s = P.f[0].z;
|
||||
let d = vec2<f32>(f32(p.x) + 0.5 - c.x, f32(p.y) + 0.5 - c.y);
|
||||
let len = sqrt(d.x * d.x + d.y * d.y) * s;
|
||||
let n = clamp(u32(ceil(len)), 2u, 64u);
|
||||
var acc = vec4<f32>(0.0);
|
||||
if (mode == 1u) {
|
||||
acc = vec4<f32>(-3.0e38);
|
||||
} else if (mode == 2u) {
|
||||
acc = vec4<f32>(3.0e38);
|
||||
}
|
||||
for (var i = 0u; i < n; i++) {
|
||||
let k = 1.0 - s * f32(i) / f32(n - 1u);
|
||||
let q = sample_bilinear(src, c.x + d.x * k, c.y + d.y * k);
|
||||
if (mode == 1u) {
|
||||
acc = max(acc, q);
|
||||
} else if (mode == 2u) {
|
||||
acc = min(acc, q);
|
||||
} else {
|
||||
acc += q / f32(n);
|
||||
}
|
||||
}
|
||||
let a = acc.w;
|
||||
textureStore(out, p, vec4<f32>(max(min(acc.xyz, vec3<f32>(a)), vec3<f32>(0.0)), clamp(a, 0.0, 1.0)));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Camera Lens Blur
|
||||
|
||||
// Linear working space and highlight boost before the blur.
|
||||
// u[0] = (linear, boost); f[0] = (threshold, boost, saturation).
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn gen_lens_pre(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let dims = out_dims();
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (p.x >= dims.x || p.y >= dims.y) {
|
||||
return;
|
||||
}
|
||||
var px = textureLoad(src, p, 0);
|
||||
if (P.u[0].x != 0u) {
|
||||
let u = gen_unpremul(px);
|
||||
px = vec4<f32>(srgb_to_linear(u.x) * u.w, srgb_to_linear(u.y) * u.w, srgb_to_linear(u.z) * u.w, u.w);
|
||||
}
|
||||
if (P.u[0].y != 0u) {
|
||||
let u = gen_unpremul(px);
|
||||
let c = u.xyz;
|
||||
let a = u.w;
|
||||
if (a > 0.0 && luminance(c) >= P.f[0].x) {
|
||||
let boost = P.f[0].y;
|
||||
let sat = P.f[0].z;
|
||||
let m = max(max(c.x, c.y), c.z);
|
||||
let tinted = c + (m - c) * (1.0 - sat);
|
||||
px = vec4<f32>((c + tinted * (boost - 1.0)) * a, px.w);
|
||||
}
|
||||
}
|
||||
textureStore(out, p, px);
|
||||
}
|
||||
|
||||
// Per-row prefix sums: out(x + 1, y) = sum of src(0..=x, y), out(0, y) = 0. One thread per row.
|
||||
@compute @workgroup_size(64, 1)
|
||||
fn gen_row_prefix(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let dims = vec2<i32>(textureDimensions(src));
|
||||
let y = i32(gid.x);
|
||||
if (y >= dims.y) {
|
||||
return;
|
||||
}
|
||||
var acc = vec4<f32>(0.0);
|
||||
textureStore(out, vec2<i32>(0, y), acc);
|
||||
for (var x = 0; x < dims.x; x++) {
|
||||
acc += textureLoad(src, vec2<i32>(x, y), 0);
|
||||
textureStore(out, vec2<i32>(x + 1, y), acc);
|
||||
}
|
||||
}
|
||||
|
||||
// The iris sum. data = spans (dy, left, right); aux = row prefix sums (when u[0].w).
|
||||
// u[0] = (spans, repeat, linear, prefix); f[0].x = 1 / pixel count.
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn gen_lens_blur(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let dims = out_dims();
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (p.x >= dims.x || p.y >= dims.y) {
|
||||
return;
|
||||
}
|
||||
let n = P.u[0].x;
|
||||
let repeat = P.u[0].y != 0u;
|
||||
let prefix = P.u[0].w != 0u;
|
||||
let w = dims.x;
|
||||
let h = dims.y;
|
||||
var acc = vec4<f32>(0.0);
|
||||
for (var s = 0u; s < n; s++) {
|
||||
var yy = p.y + i32(data[3u * s]);
|
||||
if (yy < 0 || yy >= h) {
|
||||
if (!repeat) {
|
||||
continue;
|
||||
}
|
||||
yy = clamp(yy, 0, h - 1);
|
||||
}
|
||||
var a = p.x + i32(data[3u * s + 1u]);
|
||||
var bnd = p.x + i32(data[3u * s + 2u]);
|
||||
if (!prefix) {
|
||||
for (var i = a; i <= bnd; i++) {
|
||||
if (repeat) {
|
||||
acc += textureLoad(src, vec2<i32>(clamp(i, 0, w - 1), yy), 0);
|
||||
} else if (i >= 0 && i < w) {
|
||||
acc += textureLoad(src, vec2<i32>(i, yy), 0);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (repeat) {
|
||||
if (a < 0) {
|
||||
let k = min(-a, bnd - a + 1);
|
||||
acc += textureLoad(src, vec2<i32>(0, yy), 0) * f32(k);
|
||||
a = 0;
|
||||
}
|
||||
if (bnd >= w) {
|
||||
let k = min(bnd - (w - 1), bnd - a + 1);
|
||||
acc += textureLoad(src, vec2<i32>(w - 1, yy), 0) * f32(k);
|
||||
bnd = w - 1;
|
||||
}
|
||||
} else {
|
||||
a = max(a, 0);
|
||||
bnd = min(bnd, w - 1);
|
||||
}
|
||||
if (a <= bnd) {
|
||||
acc += textureLoad(aux, vec2<i32>(bnd + 1, yy), 0) - textureLoad(aux, vec2<i32>(a, yy), 0);
|
||||
}
|
||||
}
|
||||
var px = acc * P.f[0].x;
|
||||
px.w = clamp(px.w, 0.0, 1.0);
|
||||
if (P.u[0].z != 0u) {
|
||||
let u = gen_unpremul(px);
|
||||
px = vec4<f32>(linear_to_srgb(max(u.x, 0.0)) * u.w, linear_to_srgb(max(u.y, 0.0)) * u.w, linear_to_srgb(max(u.z, 0.0)) * u.w, u.w);
|
||||
}
|
||||
textureStore(out, p, px);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- wipes
|
||||
|
||||
// Linear Wipe. data = per column ((x + 0.5)·dx − edge) (hi, lo), then per row ((y + 0.5)·dy)
|
||||
// (hi, lo); u[0].x = row table offset; f[0].x = feather.
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn gen_linear_wipe(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let dims = out_dims();
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (p.x >= dims.x || p.y >= dims.y) {
|
||||
return;
|
||||
}
|
||||
let d = gen_sum(gen_hl(2u * u32(p.x)), gen_hl(P.u[0].x + 2u * u32(p.y)));
|
||||
let k = clamp(d / P.f[0].x + 0.5, 0.0, 1.0);
|
||||
textureStore(out, p, textureLoad(src, p, 0) * k);
|
||||
}
|
||||
|
||||
// Venetian Blinds. Per column (6 floats): (cx − done·w), (cx − w − done·w), (cx − w) as
|
||||
// (hi, lo), with cx = ((x + 0.5)·dx) mod w; per row (2 floats): ((y + 0.5)·dy) mod w.
|
||||
// u[0].x = row table offset; f[0].x = feather.
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn gen_venetian(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let dims = out_dims();
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (p.x >= dims.x || p.y >= dims.y) {
|
||||
return;
|
||||
}
|
||||
let col = 6u * u32(p.x);
|
||||
let row = gen_hl(P.u[0].x + 2u * u32(p.y));
|
||||
// (d mod w) wraps once the column and row terms reach w.
|
||||
let wrapped = gen_sum(gen_hl(col + 4u), row) >= 0.0;
|
||||
var t: f32;
|
||||
if (wrapped) {
|
||||
t = gen_sum(gen_hl(col + 2u), row);
|
||||
} else {
|
||||
t = gen_sum(gen_hl(col), row);
|
||||
}
|
||||
let k = clamp(t / P.f[0].x + 0.5, 0.0, 1.0);
|
||||
textureStore(out, p, textureLoad(src, p, 0) * k);
|
||||
}
|
||||
|
||||
// Angle (degrees, clockwise from up) of the pixel vector rotated by rotation `r` (0 = start,
|
||||
// 1 = start + edge, 2 = start − edge), from the per-column / per-row tables of Radial Wipe.
|
||||
fn gen_psi(r: u32, x: u32, y: u32) -> f32 {
|
||||
let c = 9u * x + 3u * r;
|
||||
let w = P.u[0].x + 9u * y + 3u * r;
|
||||
// x' = ux·cos R − uy·sin R (cancels at the edge), y' = uy·cos R + ux·sin R.
|
||||
let xr = (data[c] - data[w]) + (data[c + 1u] - data[w + 1u]);
|
||||
let yr = data[w + 2u] + data[c + 2u];
|
||||
return atan2(xr, yr) * GEN_DEG;
|
||||
}
|
||||
|
||||
// Radial Wipe. data: per column 9 floats (for each rotation: ux·cos R (hi, lo), ux·sin R),
|
||||
// per row 9 floats (uy·sin R (hi, lo), uy·cos R); u[0] = (row offset, wipe);
|
||||
// f[0] = (edge, feather).
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn gen_radial_wipe(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let dims = out_dims();
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (p.x >= dims.x || p.y >= dims.y) {
|
||||
return;
|
||||
}
|
||||
let x = u32(p.x);
|
||||
let y = u32(p.y);
|
||||
let dir = P.u[0].y;
|
||||
let edge = P.f[0].x;
|
||||
var a = gen_psi(0u, x, y);
|
||||
if (a < 0.0) {
|
||||
a += 360.0;
|
||||
}
|
||||
var base: f32;
|
||||
if (dir == 0u || (dir == 2u && a <= 180.0)) {
|
||||
base = a - edge;
|
||||
if (abs(base) < 90.0) {
|
||||
base = gen_psi(1u, x, y);
|
||||
}
|
||||
} else {
|
||||
base = (360.0 - a) - edge;
|
||||
if (abs(base) < 90.0) {
|
||||
base = -gen_psi(2u, x, y);
|
||||
}
|
||||
}
|
||||
let k = clamp(base / P.f[0].y + 0.5, 0.0, 1.0);
|
||||
textureStore(out, p, textureLoad(src, p, 0) * k);
|
||||
}
|
||||
|
||||
// Gradient Wipe: aux = the placed gradient layer. u[0].x = invert; f[0] = (done, softness).
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn gen_gradient_wipe(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let dims = out_dims();
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (p.x >= dims.x || p.y >= dims.y) {
|
||||
return;
|
||||
}
|
||||
let done = P.f[0].x;
|
||||
let soft = P.f[0].y;
|
||||
let g = gen_unpremul(textureLoad(aux, p, 0));
|
||||
var t = clamp(luminance(g.xyz), 0.0, 1.0);
|
||||
if (P.u[0].x != 0u) {
|
||||
t = 1.0 - t;
|
||||
}
|
||||
var k = 0.0;
|
||||
if (done >= 1.0) {
|
||||
k = 0.0;
|
||||
} else if (soft > 0.0) {
|
||||
k = clamp((t - (done * (1.0 + soft) - soft)) / soft, 0.0, 1.0);
|
||||
} else if (t >= done) {
|
||||
k = 1.0;
|
||||
}
|
||||
textureStore(out, p, textureLoad(src, p, 0) * k);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Cell Pattern (generate2)
|
||||
|
||||
// Worley distances for (u, v) in cell units: (F1, F2, hash of the nearest cell).
|
||||
fn gen_worley(u: f32, v: f32, disperse: f32, evo: f32, seed: u32) -> vec3<f32> {
|
||||
let tile = P.u[1].x != 0u;
|
||||
let nx = i32(P.u[1].y);
|
||||
let ny = i32(P.u[1].z);
|
||||
let cx = i32(floor(u));
|
||||
let cy = i32(floor(v));
|
||||
var f1 = 3.0e38;
|
||||
var f2 = 3.0e38;
|
||||
var id = 0.0;
|
||||
for (var j = -1; j <= 1; j++) {
|
||||
for (var i = -1; i <= 1; i++) {
|
||||
let gx = cx + i;
|
||||
let gy = cy + j;
|
||||
var tx = gx;
|
||||
var ty = gy;
|
||||
if (tile) {
|
||||
tx = ((gx % nx) + nx) % nx;
|
||||
ty = ((gy % ny) + ny) % ny;
|
||||
}
|
||||
let hx = bitcast<u32>(tx);
|
||||
let hy = bitcast<u32>(ty);
|
||||
let h1 = hash_noise(hx, hy, seed);
|
||||
let h2 = hash_noise(hx, hy, seed + 1u);
|
||||
let h3 = hash_noise(hx, hy, seed + 2u);
|
||||
let ang = 6.283185307179586 * (h3 + evo);
|
||||
let px = f32(gx) + 0.5 + (h1 - 0.5) * disperse + 0.15 * disperse * cos(ang);
|
||||
let py = f32(gy) + 0.5 + (h2 - 0.5) * disperse + 0.15 * disperse * sin(ang);
|
||||
let d = sqrt((px - u) * (px - u) + (py - v) * (py - v));
|
||||
if (d < f1) {
|
||||
f2 = f1;
|
||||
f1 = d;
|
||||
id = hash_noise(hx, hy, seed + 3u);
|
||||
} else if (d < f2) {
|
||||
f2 = d;
|
||||
}
|
||||
}
|
||||
}
|
||||
return vec3<f32>(f1, f2, id);
|
||||
}
|
||||
|
||||
fn gen_cell_value(pattern: u32, f1: f32, f2: f32, id: f32) -> f32 {
|
||||
let e = f2 - f1;
|
||||
switch pattern {
|
||||
case 0u: { return clamp(1.0 - f1 * 1.3, 0.0, 1.0); }
|
||||
case 1u: { return id * 0.8 + 0.2 * clamp(1.0 - f1, 0.0, 1.0); }
|
||||
case 2u: { return clamp(e * 2.0, 0.0, 1.0); }
|
||||
case 3u: { return id * gen_smoothstep(0.0, 0.06, e); }
|
||||
case 4u: { return id; }
|
||||
case 5u: { return clamp(1.0 - f1 * f1 * 2.0, 0.0, 1.0); }
|
||||
case 6u: { return id * clamp(e * 3.0, 0.0, 1.0); }
|
||||
default: { return 1.0 - clamp(abs(e - 0.15) * 5.0, 0.0, 1.0); }
|
||||
}
|
||||
}
|
||||
|
||||
fn gen_overflow(v: f32, mode: u32) -> f32 {
|
||||
if (mode == 1u) {
|
||||
return 0.5 + 0.5 * tanh((v - 0.5) * 2.0);
|
||||
}
|
||||
if (mode == 2u) {
|
||||
let t = v - 2.0 * floor(v * 0.5);
|
||||
return select(t, 2.0 - t, t > 1.0);
|
||||
}
|
||||
return clamp(v, 0.0, 1.0);
|
||||
}
|
||||
|
||||
// u[0] = (pattern kind, overflow, invert, seed); u[1] = (tile, columns, rows);
|
||||
// f[0] = (contrast, disperse, size, evolution mod 1); f[1] = (0.5 − offset x, 0.5 − offset y).
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn gen_cell_pattern(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let dims = out_dims();
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (p.x >= dims.x || p.y >= dims.y) {
|
||||
return;
|
||||
}
|
||||
let size = P.f[0].z;
|
||||
let u = (f32(p.x) + P.f[1].x) / size;
|
||||
let v = (f32(p.y) + P.f[1].y) / size;
|
||||
let w = gen_worley(u, v, P.f[0].y, P.f[0].w, P.u[0].w);
|
||||
var val = gen_cell_value(P.u[0].x, w.x, w.y, w.z);
|
||||
val = gen_overflow((val - 0.5) * P.f[0].x + 0.5, P.u[0].y);
|
||||
if (P.u[0].z != 0u) {
|
||||
val = 1.0 - val;
|
||||
}
|
||||
textureStore(out, p, vec4<f32>(val, val, val, 1.0));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Checkerboard / Grid / 4-Color
|
||||
|
||||
// Signed distance (in feather units) to the nearest cell edge, positive in even cells.
|
||||
fn gen_axis(q: f32, size: f32, f: f32) -> f32 {
|
||||
let u = q / size;
|
||||
let fl = floor(u);
|
||||
let fr = u - fl;
|
||||
let d = min(fr, 1.0 - fr) * size;
|
||||
let n = i32(fl);
|
||||
let s = select(-1.0, 1.0, ((n % 2) + 2) % 2 == 0);
|
||||
return s * min(d / (f * 0.5), 1.0);
|
||||
}
|
||||
|
||||
// u[0].x = mode; f[0] = (0.5 − anchor x, 0.5 − anchor y, cell w, cell h);
|
||||
// f[1] = (feather w, feather h, opacity); f[2] = colour.
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn gen_checker(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let dims = out_dims();
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (p.x >= dims.x || p.y >= dims.y) {
|
||||
return;
|
||||
}
|
||||
let sx = gen_axis(f32(p.x) + P.f[0].x, P.f[0].z, P.f[1].x);
|
||||
let sy = gen_axis(f32(p.y) + P.f[0].y, P.f[0].w, P.f[1].y);
|
||||
let cov = clamp(0.5 + 0.5 * sx * sy, 0.0, 1.0) * P.f[1].z;
|
||||
let c = P.f[2].xyz;
|
||||
textureStore(out, p, gen_blend(textureLoad(src, p, 0), vec4<f32>(c * cov, cov), P.u[0].x));
|
||||
}
|
||||
|
||||
// f32::rem_euclid.
|
||||
fn gen_rem(x: f32, m: f32) -> f32 {
|
||||
let r = x - m * trunc(x / m);
|
||||
return select(r, r + m, r < 0.0);
|
||||
}
|
||||
|
||||
// u[0] = (mode, invert); f[0] = (0.5 − anchor x, 0.5 − anchor y, cell w, cell h);
|
||||
// f[1] = (feather w, feather h, opacity, border); f[2] = colour.
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn gen_grid(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let dims = out_dims();
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (p.x >= dims.x || p.y >= dims.y) {
|
||||
return;
|
||||
}
|
||||
let w = P.f[0].z;
|
||||
let h = P.f[0].w;
|
||||
let border = P.f[1].w;
|
||||
let gx = gen_rem(f32(p.x) + P.f[0].x, w);
|
||||
let gy = gen_rem(f32(p.y) + P.f[0].y, h);
|
||||
let dx = min(gx, w - gx);
|
||||
let dy = min(gy, h - gy);
|
||||
let cx = clamp((border / 2.0 - dx) / P.f[1].x + 0.5, 0.0, 1.0);
|
||||
let cy = clamp((border / 2.0 - dy) / P.f[1].y + 0.5, 0.0, 1.0);
|
||||
var cov = 0.0;
|
||||
if (border > 0.0) {
|
||||
cov = max(cx, cy);
|
||||
}
|
||||
if (P.u[0].y != 0u) {
|
||||
cov = 1.0 - cov;
|
||||
}
|
||||
let a = cov * P.f[1].z;
|
||||
textureStore(out, p, gen_blend(textureLoad(src, p, 0), vec4<f32>(P.f[2].xyz * a, a), P.u[0].x));
|
||||
}
|
||||
|
||||
// u[0] = (mode, seed); f[0..4] = points (x, y); f[4..8] = colours;
|
||||
// f[8] = (blend · 100, jitter, opacity).
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn gen_fourcolor(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let dims = out_dims();
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (p.x >= dims.x || p.y >= dims.y) {
|
||||
return;
|
||||
}
|
||||
let bl = P.f[8].x;
|
||||
var acc = vec3<f32>(0.0);
|
||||
var wsum = 0.0;
|
||||
for (var i = 0; i < 4; i++) {
|
||||
let q = P.f[i].xy;
|
||||
let dx = f32(p.x) + 0.5 - q.x;
|
||||
let dy = f32(p.y) + 0.5 - q.y;
|
||||
let d2 = dx * dx + dy * dy;
|
||||
let w = 1.0 / pow(d2 / bl + 1e-6, 1.5);
|
||||
acc += P.f[4 + i].xyz * w;
|
||||
wsum += w;
|
||||
}
|
||||
var c = acc / wsum;
|
||||
let jitter = P.f[8].y;
|
||||
if (jitter > 0.0) {
|
||||
let n = (hash_noise(u32(p.x), u32(p.y), P.u[0].y) - 0.5) * jitter * (8.0 / 255.0);
|
||||
c += vec3<f32>(n);
|
||||
}
|
||||
let op = P.f[8].z;
|
||||
textureStore(out, p, gen_blend(textureLoad(src, p, 0), vec4<f32>(c * op, op), P.u[0].x));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Noise / Add Grain
|
||||
|
||||
// Noise (misc::noise). u[0] = (frame seed, colour, clip, width); f[0].x = amount.
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn gen_noise(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let dims = out_dims();
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (p.x >= dims.x || p.y >= dims.y) {
|
||||
return;
|
||||
}
|
||||
var px = textureLoad(src, p, 0);
|
||||
let a = px.w;
|
||||
if (a <= 0.0) {
|
||||
textureStore(out, p, px);
|
||||
return;
|
||||
}
|
||||
let seed = P.u[0].x;
|
||||
let w = P.u[0].w;
|
||||
let amt = P.f[0].x;
|
||||
let x = u32(p.x);
|
||||
let y = u32(p.y);
|
||||
var n = vec3<f32>((hash_noise(x, y, seed) - 0.5) * amt);
|
||||
if (P.u[0].y != 0u) {
|
||||
n.y = (hash_noise(x + w, y, seed) - 0.5) * amt;
|
||||
n.z = (hash_noise(x + 2u * w, y, seed) - 0.5) * amt;
|
||||
}
|
||||
if (P.u[0].z != 0u) {
|
||||
px = vec4<f32>(clamp(px.xyz + n * a, vec3<f32>(0.0), vec3<f32>(a)), a);
|
||||
} else {
|
||||
// Unclipped: values past black / white wrap around.
|
||||
px = vec4<f32>(fract_euclid(px.x / a + n.x) * a, fract_euclid(px.y / a + n.y) * a, fract_euclid(px.z / a + n.z) * a, a);
|
||||
}
|
||||
textureStore(out, p, px);
|
||||
}
|
||||
|
||||
// Add Grain's noise planes (GrainLook::planes before Softness): rgb = per-channel zero-mean
|
||||
// value noise. u[0] = (mono, seed, salt); f[0].xyz = x divisors (size · aspect), f[0].w = frame;
|
||||
// f[1].xyz = y divisors (size).
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn gen_grain_planes(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let dims = out_dims();
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (p.x >= dims.x || p.y >= dims.y) {
|
||||
return;
|
||||
}
|
||||
let x = f32(p.x);
|
||||
let y = f32(p.y);
|
||||
let frame = P.f[0].w;
|
||||
let seed = P.u[0].y;
|
||||
let salt = P.u[0].z;
|
||||
let n0 = value_noise(x / P.f[0].x, y / P.f[1].x, frame, seed) - 0.5;
|
||||
var g = vec3<f32>(n0);
|
||||
if (P.u[0].x == 0u) {
|
||||
g.y = value_noise(x / P.f[0].y, y / P.f[1].y, frame, seed + salt) - 0.5;
|
||||
g.z = value_noise(x / P.f[0].z, y / P.f[1].z, frame, seed + 2u * salt) - 0.5;
|
||||
}
|
||||
textureStore(out, p, vec4<f32>(g, 0.0));
|
||||
}
|
||||
|
||||
// GrainLook::blend.
|
||||
fn gen_grain_blend(mode: u32, c: f32, n: f32) -> f32 {
|
||||
var v: f32;
|
||||
switch mode {
|
||||
case 1u: { v = c * (1.0 + n); }
|
||||
case 2u: { v = c + n; }
|
||||
case 3u: { v = 1.0 - (1.0 - c) * (1.0 - abs(n)); }
|
||||
case 4u: { v = c + n * 4.0 * c * (1.0 - c); }
|
||||
default: { v = c + n * (1.0 - 0.5 * c); }
|
||||
}
|
||||
return max(v, 0.0);
|
||||
}
|
||||
|
||||
// Add Grain applied: aux = noise planes. u[0] = (mono, mode, tint); f[0] = (amp, saturation,
|
||||
// midpoint); f[1].xyz = tint factors; f[2].xyz = channel intensities; f[3].xyz = tone weights.
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn gen_grain_apply(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let dims = out_dims();
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (p.x >= dims.x || p.y >= dims.y) {
|
||||
return;
|
||||
}
|
||||
let px = textureLoad(src, p, 0);
|
||||
let u = gen_unpremul(px);
|
||||
let a = u.w;
|
||||
if (a <= 0.0) {
|
||||
textureStore(out, p, px);
|
||||
return;
|
||||
}
|
||||
let c = u.xyz;
|
||||
var g = textureLoad(aux, p, 0).xyz;
|
||||
if (P.u[0].x != 0u) {
|
||||
g = vec3<f32>(g.x);
|
||||
} else {
|
||||
let gm = (g.x + g.y + g.z) / 3.0;
|
||||
g = gm + (g - gm) * P.f[0].y;
|
||||
}
|
||||
if (P.u[0].z != 0u) {
|
||||
g = g * P.f[1].xyz;
|
||||
}
|
||||
let mid = P.f[0].z;
|
||||
let l = clamp(luminance(c), 0.0, 1.0);
|
||||
let s = 1.0 - gen_smoothstep(0.0, mid, l);
|
||||
let hi = gen_smoothstep(mid, 1.0, l);
|
||||
let tones = P.f[3].xyz;
|
||||
let weight = tones.x * s + tones.y * (1.0 - s - hi) + tones.z * hi;
|
||||
let amp = P.f[0].x;
|
||||
let ci = P.f[2].xyz;
|
||||
let mode = P.u[0].y;
|
||||
let o = vec3<f32>(
|
||||
gen_grain_blend(mode, c.x, g.x * amp * ci.x * weight),
|
||||
gen_grain_blend(mode, c.y, g.y * amp * ci.y * weight),
|
||||
gen_grain_blend(mode, c.z, g.z * amp * ci.z * weight),
|
||||
);
|
||||
textureStore(out, p, vec4<f32>(o * a, a));
|
||||
}
|
||||
|
|
@ -828,3 +828,19 @@ fn pointwise(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|||
}
|
||||
textureStore(out, p, px);
|
||||
}
|
||||
|
||||
// Adjustment layer finish (Renderer::draw_adjustment): canvas `src` moves toward the adjusted
|
||||
// comp `aux` by the footprint's alpha × opacity. The footprint is `data` (RGBA f32 rows of
|
||||
// u[0].x pixels); f[0].x = opacity.
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn adjust_mix(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let dims = out_dims();
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (p.x >= dims.x || p.y >= dims.y) {
|
||||
return;
|
||||
}
|
||||
let c = textureLoad(src, p, 0);
|
||||
let a = textureLoad(aux, p, 0);
|
||||
let k = data[(gid.y * P.u[0].x + gid.x) * 4u + 3u] * P.f[0].x;
|
||||
textureStore(out, p, c + (a - c) * k);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,270 @@
|
|||
// GPU particles: effects::psim systems, one invocation per particle id. Mirrors the CPU step
|
||||
// (effects::sim::Phys::spawn / step, effects::sim3::Pg::spawn / step) operation for operation.
|
||||
//
|
||||
// Concatenated after common.wgsl and kernels.wgsl (hash_noise, value_noise, powz); bindings in
|
||||
// group 1 (group 0 is unused here).
|
||||
|
||||
struct PSim {
|
||||
// Particles, from step, to step, system (0 engine, 1 cannon).
|
||||
u: vec4<u32>,
|
||||
// Animation, flat, seed, particles in `prev`.
|
||||
u2: vec4<u32>,
|
||||
// Engine: f[0] = (life, speed, extra, cone), f[1] = producer, f[2] = radius,
|
||||
// f[3] = gravity, f[4] = axis, f[5] = (damp, dt).
|
||||
// Cannon: f[0] = (pos.x, pos.y, barrel angle, barrel radius), f[1] = (dir spread, vel,
|
||||
// vel spread, grav spread), f[2] = (gravity.x, gravity.y, dt), f[3] = bounds.
|
||||
f: array<vec4<f32>, 6>,
|
||||
}
|
||||
|
||||
struct PState {
|
||||
// Position, age.
|
||||
p: vec4<f32>,
|
||||
// Velocity, life.
|
||||
v: vec4<f32>,
|
||||
// rnd, alive (1 / 0), id.
|
||||
m: vec4<f32>,
|
||||
}
|
||||
|
||||
@group(1) @binding(0) var<uniform> S: PSim;
|
||||
@group(1) @binding(1) var<storage, read> births: array<u32>;
|
||||
@group(1) @binding(2) var<storage, read> prev: array<PState>;
|
||||
@group(1) @binding(3) var<storage, read_write> next: array<PState>;
|
||||
|
||||
const PS_TAU: f32 = 6.2831855;
|
||||
|
||||
fn ps_h(i: u32, k: u32) -> f32 {
|
||||
return hash_noise(i, k, S.u2.z);
|
||||
}
|
||||
|
||||
fn ps_hs(i: u32, k: u32) -> f32 {
|
||||
return hash_noise(i, k, S.u2.z) * 2.0 - 1.0;
|
||||
}
|
||||
|
||||
// sim::norm3.
|
||||
fn ps_norm3(v: vec3<f32>) -> vec3<f32> {
|
||||
let l = sqrt(v.x * v.x + v.y * v.y + v.z * v.z);
|
||||
if (l > 1e-9) {
|
||||
return v / l;
|
||||
}
|
||||
return vec3<f32>(0.0, -1.0, 0.0);
|
||||
}
|
||||
|
||||
// sim::rand_dir.
|
||||
fn ps_rand_dir(i: u32, k: u32, flat: bool) -> vec3<f32> {
|
||||
if (flat) {
|
||||
let a = ps_h(i, k) * PS_TAU;
|
||||
return vec3<f32>(cos(a), sin(a), 0.0);
|
||||
}
|
||||
let z = ps_hs(i, k);
|
||||
let a = ps_h(i, k + 1u) * PS_TAU;
|
||||
let r = sqrt(max(1.0 - z * z, 0.0));
|
||||
return vec3<f32>(r * cos(a), r * sin(a), z);
|
||||
}
|
||||
|
||||
struct Part {
|
||||
p: vec3<f32>,
|
||||
v: vec3<f32>,
|
||||
age: f32,
|
||||
life: f32,
|
||||
rnd: f32,
|
||||
}
|
||||
|
||||
// sim::Phys::spawn (+ the step's sub-step birth offset).
|
||||
fn ps_spawn_engine(id: u32) -> Part {
|
||||
let flat = S.u2.y != 0u;
|
||||
let anim = S.u2.x;
|
||||
let producer = S.f[1].xyz;
|
||||
let radius = S.f[2].xyz;
|
||||
let u = ps_rand_dir(id, 1u, flat);
|
||||
var e = 1.0 / 3.0;
|
||||
if (flat) {
|
||||
e = 0.5;
|
||||
}
|
||||
let rr = powz(ps_h(id, 3u), e);
|
||||
var p = producer + u * radius * rr;
|
||||
if (flat) {
|
||||
p.z = 0.0;
|
||||
}
|
||||
let ax = S.f[4].xyz;
|
||||
let jitter = ps_rand_dir(id, 5u, flat);
|
||||
var dir = jitter;
|
||||
switch anim {
|
||||
case 1u, 9u: {
|
||||
dir = ps_norm3(ax + jitter * 0.08);
|
||||
}
|
||||
case 2u: {
|
||||
let k = S.f[0].w;
|
||||
dir = ps_norm3(ax + jitter * k * ps_h(id, 8u));
|
||||
}
|
||||
case 5u: {
|
||||
var z = jitter.z;
|
||||
if (flat) {
|
||||
z = 0.0;
|
||||
}
|
||||
dir = vec3<f32>(jitter.x, 0.0, z);
|
||||
}
|
||||
case 6u: {
|
||||
dir = ps_norm3(vec3<f32>(jitter.x * 0.25, -1.0, jitter.z * 0.25));
|
||||
}
|
||||
case 7u: {
|
||||
dir = ps_norm3(vec3<f32>(1.0, jitter.y * 0.05, jitter.z * 0.05));
|
||||
}
|
||||
default: {}
|
||||
}
|
||||
let sp = S.f[0].y * (0.5 + 0.5 * ps_h(id, 9u));
|
||||
var v = dir * sp;
|
||||
if (flat) {
|
||||
v.z = 0.0;
|
||||
}
|
||||
let life = S.f[0].x * (0.8 + 0.4 * ps_h(id, 10u));
|
||||
let dt = S.f[5].y;
|
||||
let frac = ps_h(id, 12u) * dt;
|
||||
return Part(p + v * frac, v, frac, life, ps_h(id, 11u));
|
||||
}
|
||||
|
||||
// sim::Phys::step for one particle at step `j`.
|
||||
fn ps_step_engine(q: Part, j: u32) -> Part {
|
||||
let flat = S.u2.y != 0u;
|
||||
let anim = S.u2.x;
|
||||
let seed = S.u2.z;
|
||||
let dt = S.f[5].y;
|
||||
let t = f32(j) * dt;
|
||||
let producer = S.f[1].xyz;
|
||||
let gravity = S.f[3].xyz;
|
||||
let extra = S.f[0].z;
|
||||
var a = gravity;
|
||||
switch anim {
|
||||
case 4u: {
|
||||
let dx = q.p.x - producer.x;
|
||||
let dz = q.p.z - producer.z;
|
||||
let k = 4.0 * extra;
|
||||
a.x += -dz * k;
|
||||
a.z += dx * k;
|
||||
if (flat) {
|
||||
let dy = q.p.y - producer.y;
|
||||
a.x += -dy * k;
|
||||
a.y += dx * k;
|
||||
}
|
||||
}
|
||||
case 5u: {
|
||||
let dx = q.p.x - producer.x;
|
||||
let dy = q.p.y - producer.y;
|
||||
let k = 6.0 * extra;
|
||||
a.x += -dy * k - dx * 2.0;
|
||||
a.y += dx * k - dy * 2.0;
|
||||
}
|
||||
case 6u: {
|
||||
let n = value_noise(q.p.x * 8.0, q.p.y * 8.0, t * 3.0, seed) - 0.5;
|
||||
a.x += n * 4.0 * extra;
|
||||
a.y -= gravity.y * 1.5 + 0.3 * extra;
|
||||
}
|
||||
case 8u, 9u: {
|
||||
let f = 6.0 * extra;
|
||||
a.x += (value_noise(q.p.x * 5.0, q.p.y * 5.0, q.p.z * 5.0 + t, seed) - 0.5) * f;
|
||||
a.y += (value_noise(q.p.x * 5.0 + 17.0, q.p.y * 5.0, q.p.z * 5.0 + t, seed) - 0.5) * f;
|
||||
if (!flat) {
|
||||
a.z += (value_noise(q.p.x * 5.0, q.p.y * 5.0 + 31.0, q.p.z * 5.0 + t, seed) - 0.5) * f;
|
||||
}
|
||||
}
|
||||
default: {}
|
||||
}
|
||||
let damp = S.f[5].x;
|
||||
var o = q;
|
||||
o.v = (q.v + a * dt) * damp;
|
||||
o.p = q.p + o.v * dt;
|
||||
if (flat) {
|
||||
o.p.z = 0.0;
|
||||
o.v.z = 0.0;
|
||||
}
|
||||
o.age = q.age + dt;
|
||||
return o;
|
||||
}
|
||||
|
||||
// sim3::Pg::spawn (+ the sub-step birth offset).
|
||||
fn ps_spawn_cannon(id: u32) -> Part {
|
||||
let pos = S.f[0].xy;
|
||||
let angle = S.f[0].z;
|
||||
let r = S.f[0].w;
|
||||
let spread = S.f[1].x;
|
||||
let vel = S.f[1].y;
|
||||
let vel_spread = S.f[1].z;
|
||||
let a = radians(angle + ps_hs(id, 1u) * spread * 0.5);
|
||||
let d = vec2<f32>(sin(a), -cos(a));
|
||||
let speed = max(vel + ps_hs(id, 3u) * vel_spread, 0.0);
|
||||
var off: vec2<f32>;
|
||||
if (r >= 0.0) {
|
||||
off = vec2<f32>(ps_hs(id, 2u) * r, ps_hs(id, 4u) * r);
|
||||
} else {
|
||||
let aa = ps_h(id, 5u) * PS_TAU;
|
||||
let rr = sqrt(ps_h(id, 6u)) * -r;
|
||||
off = vec2<f32>(cos(aa) * rr, sin(aa) * rr);
|
||||
}
|
||||
let v = d * speed;
|
||||
let dt = S.f[2].z;
|
||||
let frac = ps_h(id, 9u) * dt;
|
||||
let p = pos + off + v * frac;
|
||||
return Part(vec3<f32>(p, 0.0), vec3<f32>(v, 0.0), 0.0, 0.0, 0.0);
|
||||
}
|
||||
|
||||
// sim3::Pg::step for one particle (gravity only); returns false when it leaves the bounds.
|
||||
fn ps_step_cannon(q: ptr<function, Part>, id: u32) -> bool {
|
||||
let dt = S.f[2].z;
|
||||
let spread = S.f[1].w;
|
||||
var g = 1.0;
|
||||
if (spread > 0.0) {
|
||||
g = 1.0 + (ps_h(id, 7u) * 2.0 - 1.0) * spread;
|
||||
}
|
||||
let a = S.f[2].xy * g;
|
||||
let v = ((*q).v.xy + a * dt) * 1.0;
|
||||
let p = (*q).p.xy + v * dt;
|
||||
(*q).v = vec3<f32>(v, 0.0);
|
||||
(*q).p = vec3<f32>(p, 0.0);
|
||||
let b = S.f[3];
|
||||
return p.x > b.x && p.x < b.z && p.y > b.y && p.y < b.w;
|
||||
}
|
||||
|
||||
@compute @workgroup_size(64, 1)
|
||||
fn particles(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let id = gid.x;
|
||||
if (id >= S.u.x) {
|
||||
return;
|
||||
}
|
||||
let s0 = S.u.y;
|
||||
let s1 = S.u.z;
|
||||
let cannon = S.u.w == 1u;
|
||||
let b = births[id];
|
||||
var q: Part;
|
||||
var j: u32;
|
||||
if (b < s0 && id < S.u2.w) {
|
||||
let o = prev[id];
|
||||
if (o.m.y == 0.0) {
|
||||
next[id] = o;
|
||||
return;
|
||||
}
|
||||
q = Part(o.p.xyz, o.v.xyz, o.p.w, o.v.w, o.m.x);
|
||||
j = s0;
|
||||
} else {
|
||||
if (cannon) {
|
||||
q = ps_spawn_cannon(id);
|
||||
} else {
|
||||
q = ps_spawn_engine(id);
|
||||
}
|
||||
j = b + 1u;
|
||||
}
|
||||
var alive = true;
|
||||
for (; j < s1; j++) {
|
||||
if (cannon) {
|
||||
if (!ps_step_cannon(&q, id)) {
|
||||
alive = false;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
q = ps_step_engine(q, j);
|
||||
if (q.age >= q.life) {
|
||||
alive = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
next[id] = PState(vec4<f32>(q.p, q.age), vec4<f32>(q.v, q.life), vec4<f32>(q.rnd, select(0.0, 1.0, alive), 0.0, 0.0));
|
||||
}
|
||||
|
|
@ -26,7 +26,7 @@ use effectcraft_time::{FrameRate, Tick};
|
|||
|
||||
use crate::Gpu;
|
||||
|
||||
fn gpu() -> Option<&'static Gpu> {
|
||||
pub(crate) fn gpu() -> Option<&'static Gpu> {
|
||||
static G: OnceLock<Option<Gpu>> = OnceLock::new();
|
||||
G.get_or_init(|| {
|
||||
let g = Gpu::headless();
|
||||
|
|
@ -40,9 +40,9 @@ fn gpu() -> Option<&'static Gpu> {
|
|||
|
||||
/// Procedural footage: smooth colour gradients, fine noise and an alpha with soft, opaque and
|
||||
/// fully transparent regions (different per item).
|
||||
struct Pattern;
|
||||
pub(crate) struct Pattern;
|
||||
|
||||
fn pattern(seed: u32, w: u32, h: u32) -> Image {
|
||||
pub(crate) fn pattern(seed: u32, w: u32, h: u32) -> Image {
|
||||
let mut img = Image::new(w, h);
|
||||
let k = seed as f32 * 0.37;
|
||||
for y in 0..h {
|
||||
|
|
@ -66,14 +66,14 @@ impl FootageSource for Pattern {
|
|||
}
|
||||
}
|
||||
|
||||
struct Scene {
|
||||
p: Project,
|
||||
cid: ItemId,
|
||||
comp: Comp,
|
||||
pub(crate) struct Scene {
|
||||
pub(crate) p: Project,
|
||||
pub(crate) cid: ItemId,
|
||||
pub(crate) comp: Comp,
|
||||
}
|
||||
|
||||
impl Scene {
|
||||
fn new(depth: BitDepth) -> Scene {
|
||||
pub(crate) fn new(depth: BitDepth) -> Scene {
|
||||
let mut p = Project::default();
|
||||
p.settings.bit_depth = depth;
|
||||
let comp = Comp::new(97, 61, FrameRate::FPS_30, Tick::from_seconds_f64(2.0));
|
||||
|
|
@ -81,7 +81,7 @@ impl Scene {
|
|||
Scene { p, cid, comp }
|
||||
}
|
||||
|
||||
fn footage(&mut self, w: u32, h: u32) -> Layer {
|
||||
pub(crate) fn footage(&mut self, w: u32, h: u32) -> Layer {
|
||||
let f = Footage {
|
||||
path: "pattern.png".into(),
|
||||
kind: FootageKind::Still,
|
||||
|
|
@ -106,19 +106,19 @@ impl Scene {
|
|||
build::layer(&mut self.p, &self.comp, "Pattern", LayerSource::Footage { item: fid }, (w, h), None)
|
||||
}
|
||||
|
||||
fn solid(&mut self, color: [f32; 3], w: u32, h: u32) -> Layer {
|
||||
pub(crate) fn solid(&mut self, color: [f32; 3], w: u32, h: u32) -> Layer {
|
||||
let sid = self.p.add_item("Solid", Label::Red, None, ItemKind::Solid(Solid { color, width: w, height: h, pixel_aspect: 1.0 }));
|
||||
build::layer(&mut self.p, &self.comp, "Solid", LayerSource::Solid { item: sid }, (w, h), None)
|
||||
}
|
||||
|
||||
/// Add on top of the stack.
|
||||
fn push(&mut self, l: Layer) -> effectcraft_project::LayerId {
|
||||
pub(crate) fn push(&mut self, l: Layer) -> effectcraft_project::LayerId {
|
||||
let id = l.id;
|
||||
self.p.comp_mut(self.cid).unwrap().layers.insert(0, l);
|
||||
id
|
||||
}
|
||||
|
||||
fn effect(&mut self, l: &mut Layer, id: &str, vals: &[(&str, Value)]) {
|
||||
pub(crate) fn effect(&mut self, l: &mut Layer, id: &str, vals: &[(&str, Value)]) {
|
||||
let spec = effectcraft_effects::find(id).unwrap();
|
||||
let mut next = self.p.next_id;
|
||||
let size = effectcraft_render::source_size(&self.p, l);
|
||||
|
|
@ -131,27 +131,27 @@ impl Scene {
|
|||
}
|
||||
}
|
||||
|
||||
fn set(l: &mut Layer, path: &str, v: Value) {
|
||||
pub(crate) fn set(l: &mut Layer, path: &str, v: Value) {
|
||||
l.props.prop_mut(path).unwrap_or_else(|| panic!("no {path}")).value = v;
|
||||
}
|
||||
|
||||
fn v3(x: f64, y: f64) -> Value {
|
||||
pub(crate) fn v3(x: f64, y: f64) -> Value {
|
||||
Value::Vec3([x, y, 0.0])
|
||||
}
|
||||
|
||||
/// Differences of a GPU render against the CPU render.
|
||||
#[derive(Debug)]
|
||||
struct Diff {
|
||||
max: f32,
|
||||
pub(crate) struct Diff {
|
||||
pub(crate) max: f32,
|
||||
/// Pixels over the tolerance.
|
||||
over: usize,
|
||||
total: usize,
|
||||
worst: (u32, u32, [f32; 4], [f32; 4]),
|
||||
pub(crate) over: usize,
|
||||
pub(crate) total: usize,
|
||||
pub(crate) worst: (u32, u32, [f32; 4], [f32; 4]),
|
||||
/// 8/16 bpc render.
|
||||
quantized: bool,
|
||||
pub(crate) quantized: bool,
|
||||
}
|
||||
|
||||
fn diff(cpu: &Image, gpu: &Image, tol: f32) -> Diff {
|
||||
pub(crate) fn diff(cpu: &Image, gpu: &Image, tol: f32) -> Diff {
|
||||
assert_eq!((cpu.width, cpu.height), (gpu.width, gpu.height), "size");
|
||||
let mut d = Diff { max: 0.0, over: 0, total: cpu.data.len(), worst: (0, 0, [0.0; 4], [0.0; 4]), quantized: false };
|
||||
for (i, (a, b)) in cpu.data.iter().zip(&gpu.data).enumerate() {
|
||||
|
|
@ -169,7 +169,7 @@ fn diff(cpu: &Image, gpu: &Image, tol: f32) -> Diff {
|
|||
d
|
||||
}
|
||||
|
||||
fn tolerance(depth: BitDepth) -> f32 {
|
||||
pub(crate) fn tolerance(depth: BitDepth) -> f32 {
|
||||
match depth {
|
||||
BitDepth::Bpc8 => 1.0 / 255.0 + 1e-6,
|
||||
_ => 1e-3,
|
||||
|
|
@ -177,7 +177,7 @@ fn tolerance(depth: BitDepth) -> f32 {
|
|||
}
|
||||
|
||||
/// Render on the CPU and the GPU; `Some(diff)` (None without an adapter).
|
||||
fn compare_at(s: &Scene, opts: RenderOpts, t: Tick) -> Option<Diff> {
|
||||
pub(crate) fn compare_at(s: &Scene, opts: RenderOpts, t: Tick) -> Option<Diff> {
|
||||
let g = gpu()?;
|
||||
let cpu = Renderer::new(&s.p, &Pattern, RenderOpts { backend: Backend::Cpu, ..opts }).comp_frame_cpu(s.cid, t);
|
||||
let mut r = Renderer::new(&s.p, &Pattern, RenderOpts { backend: Backend::Gpu, ..opts });
|
||||
|
|
@ -188,12 +188,12 @@ fn compare_at(s: &Scene, opts: RenderOpts, t: Tick) -> Option<Diff> {
|
|||
Some(d)
|
||||
}
|
||||
|
||||
fn opts() -> RenderOpts {
|
||||
pub(crate) fn opts() -> RenderOpts {
|
||||
RenderOpts { motion_blur: true, ..Default::default() }
|
||||
}
|
||||
|
||||
/// Assert agreement; `allow` = fraction of pixels allowed over the tolerance.
|
||||
fn check(label: &str, d: Option<Diff>, allow: f64) {
|
||||
pub(crate) fn check(label: &str, d: Option<Diff>, allow: f64) {
|
||||
let Some(d) = d else { return };
|
||||
let frac = d.over as f64 / d.total as f64;
|
||||
// Quantised depths: rounding-boundary flips (see the module docs).
|
||||
|
|
@ -379,7 +379,7 @@ fn layer_styles_collapsed_precomps_and_cpu_fallbacks() {
|
|||
|
||||
/// The effect alone on a buffer: GPU kernels vs the CPU effect (32-bit float), including the
|
||||
/// buffer geometry (padding, offset).
|
||||
fn effect_direct(id: &str, vals: &[(&str, Value)], adjustment: bool) {
|
||||
pub(crate) fn effect_direct(id: &str, vals: &[(&str, Value)], adjustment: bool) {
|
||||
let Some(g) = gpu() else { return };
|
||||
let spec = effectcraft_effects::find(id).unwrap();
|
||||
let size = [70.0, 44.0];
|
||||
|
|
@ -401,7 +401,7 @@ fn effect_direct(id: &str, vals: &[(&str, Value)], adjustment: bool) {
|
|||
}
|
||||
|
||||
/// One footage layer with `fx` applied, composited over a background, at 8 and 32 bpc.
|
||||
fn effect_case(id: &str, vals: &[(&str, Value)]) {
|
||||
pub(crate) fn effect_case(id: &str, vals: &[(&str, Value)]) {
|
||||
effect_direct(id, vals, false);
|
||||
effect_direct(id, vals, true);
|
||||
for depth in [BitDepth::Bpc8, BitDepth::Bpc32] {
|
||||
|
|
@ -417,11 +417,11 @@ fn effect_case(id: &str, vals: &[(&str, Value)]) {
|
|||
}
|
||||
}
|
||||
|
||||
fn n(v: f64) -> Value {
|
||||
pub(crate) fn n(v: f64) -> Value {
|
||||
Value::Scalar(v)
|
||||
}
|
||||
|
||||
fn c(r: f64, g: f64, b: f64) -> Value {
|
||||
pub(crate) fn c(r: f64, g: f64, b: f64) -> Value {
|
||||
Value::Color([r, g, b, 1.0])
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,220 @@
|
|||
//! Classic 3D on the GPU vs the CPU compositor: intersecting planes, blend modes, lights,
|
||||
//! ray-cast shadows, depth of field, motion blur, track mattes, collapsed precomps and
|
||||
//! adjustment layers splitting a run. Same tolerances as `tests.rs`; pixels on the exact
|
||||
//! intersection line of two planes may sort differently (f32 vs f64 depths), so intersecting
|
||||
//! scenes allow 0.5 % of pixels over.
|
||||
|
||||
use effectcraft_color::{BlendMode, Label};
|
||||
use effectcraft_keyframe::{Keyframe, Value};
|
||||
use effectcraft_project::build;
|
||||
use effectcraft_project::{BitDepth, Comp, ItemKind, Layer, LayerSource, LightKind, MatteKind, TrackMatte};
|
||||
use effectcraft_render::{Backend, RenderOpts, Renderer};
|
||||
use effectcraft_time::{FrameRate, Tick};
|
||||
|
||||
use crate::tests::{Pattern, Scene, check, compare_at, gpu, n, opts, set};
|
||||
|
||||
const W: u32 = 160;
|
||||
const H: u32 = 100;
|
||||
|
||||
fn scene(depth: BitDepth) -> Scene {
|
||||
let mut s = Scene::new(depth);
|
||||
let comp = Comp::new(W, H, FrameRate::FPS_30, Tick::from_seconds_f64(2.0));
|
||||
*s.p.comp_mut(s.cid).unwrap() = comp.clone();
|
||||
s.comp = comp;
|
||||
s
|
||||
}
|
||||
|
||||
fn three_d(mut l: Layer, pos: [f64; 3], rot_y: f64) -> Layer {
|
||||
l.switches.three_d = true;
|
||||
set(&mut l, "transform/position", Value::Vec3(pos));
|
||||
set(&mut l, "transform/rotationY", Value::Scalar(rot_y));
|
||||
l
|
||||
}
|
||||
|
||||
fn camera(s: &mut Scene, pos: [f64; 3], dof: Option<(f64, f64)>) {
|
||||
let mut cam = build::layer(&mut s.p, &s.comp, "Camera", LayerSource::Camera, (W, H), None);
|
||||
set(&mut cam, "cameraOptions/zoom", Value::Scalar(W as f64 * 50.0 / 36.0));
|
||||
set(&mut cam, "transform/position", Value::Vec3(pos));
|
||||
set(&mut cam, "transform/poi", Value::Vec3([W as f64 / 2.0, H as f64 / 2.0, 0.0]));
|
||||
if let Some((focus, aperture)) = dof {
|
||||
set(&mut cam, "cameraOptions/dof", Value::Bool(true));
|
||||
set(&mut cam, "cameraOptions/focusDistance", Value::Scalar(focus));
|
||||
set(&mut cam, "cameraOptions/aperture", Value::Scalar(aperture));
|
||||
}
|
||||
s.push(cam);
|
||||
}
|
||||
|
||||
fn light(s: &mut Scene, kind: LightKind, pos: [f64; 3], vals: &[(&str, Value)]) {
|
||||
let mut l = build::layer(&mut s.p, &s.comp, "Light", LayerSource::Light { kind }, (W, H), None);
|
||||
if kind != LightKind::Ambient {
|
||||
set(&mut l, "transform/position", Value::Vec3(pos));
|
||||
}
|
||||
if l.props.prop_mut("transform/poi").is_some() {
|
||||
set(&mut l, "transform/poi", Value::Vec3([W as f64 / 2.0, H as f64 / 2.0, 0.0]));
|
||||
}
|
||||
for (k, v) in vals {
|
||||
set(&mut l, &format!("lightOptions/{k}"), v.clone());
|
||||
}
|
||||
s.push(l);
|
||||
}
|
||||
|
||||
/// Background, a far plane, and two planes crossing each other (one in `mode`).
|
||||
fn crossing(depth: BitDepth, mode: BlendMode) -> Scene {
|
||||
let mut s = scene(depth);
|
||||
let bg = s.solid([0.1, 0.12, 0.2], W, H);
|
||||
s.push(bg);
|
||||
let far = three_d(s.footage(150, 90), [80.0, 50.0, 120.0], 0.0);
|
||||
s.push(far);
|
||||
let a = three_d(s.footage(90, 70), [80.0, 50.0, 0.0], 35.0);
|
||||
s.push(a);
|
||||
let mut b = three_d(s.solid([0.9, 0.5, 0.2], 90, 70), [80.0, 52.0, 0.0], -35.0);
|
||||
b.blend_mode = mode;
|
||||
set(&mut b, "transform/opacity", Value::Scalar(80.0));
|
||||
s.push(b);
|
||||
s
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intersecting_planes_and_blend_modes() {
|
||||
for depth in [BitDepth::Bpc8, BitDepth::Bpc32] {
|
||||
for mode in [BlendMode::Normal, BlendMode::Screen, BlendMode::Multiply, BlendMode::Add, BlendMode::Dissolve] {
|
||||
let mut s = crossing(depth, mode);
|
||||
check(&format!("crossing {mode:?} {depth:?} (default camera)"), compare_at(&s, opts(), Tick::ZERO), 0.005);
|
||||
camera(&mut s, [30.0, 20.0, -260.0], None);
|
||||
check(&format!("crossing {mode:?} {depth:?} (camera)"), compare_at(&s, opts(), Tick::ZERO), 0.005);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lights_specular_transmission_and_shadows() {
|
||||
for depth in [BitDepth::Bpc8, BitDepth::Bpc32] {
|
||||
let mut s = crossing(depth, BlendMode::Normal);
|
||||
// Every layer casts shadows; the crossing pair transmits light.
|
||||
for l in &mut s.p.comp_mut(s.cid).unwrap().layers {
|
||||
if l.switches.three_d {
|
||||
set(l, "materialOptions/castsShadows", Value::Enum(1));
|
||||
set(l, "materialOptions/specularIntensity", Value::Scalar(60.0));
|
||||
set(l, "materialOptions/specularShininess", Value::Scalar(30.0));
|
||||
set(l, "materialOptions/lightTransmission", Value::Scalar(40.0));
|
||||
}
|
||||
}
|
||||
camera(&mut s, [-20.0, -10.0, -250.0], None);
|
||||
light(&mut s, LightKind::Ambient, [0.0, 0.0, 0.0], &[("intensity", n(25.0))]);
|
||||
light(
|
||||
&mut s,
|
||||
LightKind::Spot,
|
||||
[20.0, -60.0, -150.0],
|
||||
&[("castsShadows", Value::Bool(true)), ("shadowDarkness", n(80.0)), ("coneAngle", n(70.0)), ("coneFeather", n(40.0)), ("intensity", n(120.0))],
|
||||
);
|
||||
light(
|
||||
&mut s,
|
||||
LightKind::Point,
|
||||
[150.0, 20.0, -80.0],
|
||||
&[
|
||||
("castsShadows", Value::Bool(true)),
|
||||
("shadowDiffusion", n(6.0)),
|
||||
("falloff", Value::Enum(1)),
|
||||
("radius", n(120.0)),
|
||||
("falloffDistance", n(200.0)),
|
||||
],
|
||||
);
|
||||
light(&mut s, LightKind::Parallel, [-100.0, -100.0, -100.0], &[("castsShadows", Value::Bool(true)), ("color", Value::Color([0.5, 0.7, 1.0, 1.0]))]);
|
||||
check(&format!("lights + shadows {depth:?}"), compare_at(&s, opts(), Tick::ZERO), 0.005);
|
||||
// Accepts Shadows: Only, Accepts Lights off, Casts Shadows: Only.
|
||||
for l in &mut s.p.comp_mut(s.cid).unwrap().layers {
|
||||
match l.name.as_str() {
|
||||
"Pattern" => set(l, "materialOptions/acceptsShadows", Value::Enum(2)),
|
||||
"Solid" if l.switches.three_d => set(l, "materialOptions/acceptsLights", Value::Bool(false)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
check(&format!("shadow-only materials {depth:?}"), compare_at(&s, opts(), Tick::ZERO), 0.005);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn depth_of_field_and_motion_blur() {
|
||||
for depth in [BitDepth::Bpc8, BitDepth::Bpc32] {
|
||||
let mut s = crossing(depth, BlendMode::Normal);
|
||||
camera(&mut s, [80.0, 50.0, -220.0], Some((220.0, 30.0)));
|
||||
check(&format!("dof {depth:?}"), compare_at(&s, opts(), Tick::ZERO), 0.005);
|
||||
// Animated plane with motion blur (sub-sampled geometry, equal weights).
|
||||
let mut s = scene(depth);
|
||||
let mut l = three_d(s.footage(60, 40), [40.0, 50.0, 0.0], 20.0);
|
||||
l.switches.motion_blur = true;
|
||||
l.props.prop_mut("transform/position").unwrap().keys =
|
||||
vec![Keyframe::new(Tick::ZERO, Value::Vec3([40.0, 50.0, 0.0])), Keyframe::new(Tick::from_seconds_f64(1.0), Value::Vec3([140.0, 40.0, -60.0]))];
|
||||
s.push(l);
|
||||
s.p.comp_mut(s.cid).unwrap().enable_motion_blur = true;
|
||||
light(&mut s, LightKind::Point, [80.0, 0.0, -100.0], &[]);
|
||||
check(&format!("3d motion blur {depth:?}"), compare_at(&s, opts(), Tick::from_seconds_f64(0.5)), 0.005);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mattes_preserve_transparency_precomps_and_adjustment_splits() {
|
||||
for depth in [BitDepth::Bpc8, BitDepth::Bpc32] {
|
||||
let mut s = crossing(depth, BlendMode::Normal);
|
||||
// A 2D luma matte for the top plane, preserve transparency on the far plane.
|
||||
let m = s.solid([0.7, 0.7, 0.7], 70, 50);
|
||||
let mid = s.push(m);
|
||||
let layers = &mut s.p.comp_mut(s.cid).unwrap().layers;
|
||||
layers[1].track_matte = Some(TrackMatte { layer: mid, kind: MatteKind::Luma });
|
||||
if let Some(far) = layers.iter_mut().find(|l| l.switches.three_d && l.name == "Pattern") {
|
||||
far.preserve_transparency = true;
|
||||
}
|
||||
// Move the matte below the bottom so the 3D run stays one run.
|
||||
let ml = layers.remove(0);
|
||||
layers.push(ml);
|
||||
layers.last_mut().unwrap().switches.video = false;
|
||||
check(&format!("matte + preserve {depth:?}"), compare_at(&s, opts(), Tick::ZERO), 0.005);
|
||||
|
||||
// An adjustment layer in the middle of the run splits it.
|
||||
let mut s = crossing(depth, BlendMode::Normal);
|
||||
let mut adj = s.solid([1.0, 1.0, 1.0], W, H);
|
||||
adj.switches.adjustment = true;
|
||||
adj.switches.three_d = true;
|
||||
s.effect(&mut adj, "ec.channel.invert", &[]);
|
||||
s.p.comp_mut(s.cid).unwrap().layers.insert(1, adj);
|
||||
check(&format!("adjustment split {depth:?}"), compare_at(&s, opts(), Tick::ZERO), 0.005);
|
||||
|
||||
// A collapsed 3D precomp: its layers join the parent's 3D space.
|
||||
let mut s = crossing(depth, BlendMode::Normal);
|
||||
let inner = Comp::new(80, 60, FrameRate::FPS_30, Tick::from_seconds_f64(2.0));
|
||||
let inner_id = s.p.add_item("Inner", Label::Sandstone, None, ItemKind::Comp(inner.clone().into()));
|
||||
let mut a = s.footage(50, 40);
|
||||
a.switches.three_d = true;
|
||||
set(&mut a, "transform/rotationX", Value::Scalar(40.0));
|
||||
let b = s.solid([0.2, 0.8, 0.4], 30, 20);
|
||||
s.p.comp_mut(inner_id).unwrap().layers = vec![b, a];
|
||||
let mut pre = build::layer(&mut s.p, &s.comp, "Inner", LayerSource::Comp { item: inner_id }, (80, 60), None);
|
||||
pre.switches.collapse = true;
|
||||
pre.switches.three_d = true;
|
||||
set(&mut pre, "transform/position", Value::Vec3([70.0, 45.0, -10.0]));
|
||||
s.push(pre);
|
||||
check(&format!("collapsed 3d precomp {depth:?}"), compare_at(&s, opts(), Tick::ZERO), 0.005);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_gpu_draws_classic_3d_runs_itself() {
|
||||
let Some(g) = gpu() else { return };
|
||||
let s = crossing(BitDepth::Bpc32, BlendMode::Normal);
|
||||
let r = Renderer::new(&s.p, &Pattern, RenderOpts { backend: Backend::Gpu, ..opts() });
|
||||
let ctx = r.eval_ctx(s.cid, Tick::ZERO).unwrap();
|
||||
let vis = r.visible_layers(&ctx);
|
||||
let run: Vec<_> = vis.iter().copied().filter(|l| l.is_3d()).collect();
|
||||
let prep = r.prepare_3d_run(&ctx, &run, (W, H)).expect("a classic run is prepared for the GPU");
|
||||
assert_eq!(prep.planes.iter().filter(|p| p.draw).count(), 3);
|
||||
let mut e = crate::context::Enc::new(g.context());
|
||||
let canvas = e.image(W, H);
|
||||
let out = crate::classic3d::draw_run(&mut e, &prep, &canvas).expect("the kernel draws the run");
|
||||
let img = e.download(&out).unwrap();
|
||||
let mut cpu = effectcraft_raster::Image::new(W, H);
|
||||
r.draw_3d_run(&ctx, &run, &mut cpu);
|
||||
let covered = cpu.data.iter().filter(|p| p[3] > 0.5).count();
|
||||
assert!(covered > (W * H / 3) as usize, "the planes cover the frame ({covered} px)");
|
||||
let d = crate::tests::diff(&cpu, &img, 1e-3);
|
||||
assert!(d.over * 200 <= d.total, "{d:?}");
|
||||
}
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
//! Adjustment layers on the GPU vs the CPU: GPU-only stacks (resident on the device), mixed
|
||||
//! stacks (CPU effects in between read back and upload), masks, opacity, colour management.
|
||||
|
||||
use effectcraft_keyframe::{ShapePath, Value};
|
||||
use effectcraft_project::build::{self, Ids};
|
||||
use effectcraft_project::{BitDepth, ColorSpace, MaskMode};
|
||||
use effectcraft_render::{Backend, FxStep, FxTarget, RenderOpts, Renderer};
|
||||
use effectcraft_time::Tick;
|
||||
|
||||
use crate::tests::{Pattern, Scene, c, check, compare_at, gpu, n, opts, set};
|
||||
|
||||
fn adjustment_scene(depth: BitDepth, stack: &[(&str, Vec<(&str, Value)>)], masked: bool) -> Scene {
|
||||
let mut s = Scene::new(depth);
|
||||
let bg = s.footage(97, 61);
|
||||
s.push(bg);
|
||||
let mut l = s.footage(60, 40);
|
||||
set(&mut l, "transform/rotation", Value::Scalar(15.0));
|
||||
s.push(l);
|
||||
let mut adj = s.solid([1.0, 1.0, 1.0], 80, 50);
|
||||
adj.switches.adjustment = true;
|
||||
set(&mut adj, "transform/opacity", Value::Scalar(75.0));
|
||||
set(&mut adj, "transform/rotation", Value::Scalar(-6.0));
|
||||
for (id, vals) in stack {
|
||||
s.effect(&mut adj, id, vals);
|
||||
}
|
||||
if masked {
|
||||
let mut next = s.p.next_id;
|
||||
let m = build::mask(&mut Ids(&mut next), "Mask 1", ShapePath::ellipse([40.0, 25.0], 60.0, 40.0), MaskMode::Add, [255, 255, 0]);
|
||||
s.p.next_id = next;
|
||||
adj.props.sub_mut("masks").unwrap().children.push(m.into());
|
||||
}
|
||||
s.push(adj);
|
||||
let top = s.solid([0.2, 0.6, 0.9], 20, 20);
|
||||
s.push(top);
|
||||
s
|
||||
}
|
||||
|
||||
fn gpu_stack() -> Vec<(&'static str, Vec<(&'static str, Value)>)> {
|
||||
vec![
|
||||
("ec.blur.gaussian", vec![("blurriness", n(6.0))]),
|
||||
("ec.color.levels", vec![("inBlack", n(0.1)), ("gamma", n(1.4))]),
|
||||
("ec.color.tint", vec![("black", c(0.1, 0.0, 0.2)), ("amount", n(60.0))]),
|
||||
]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gpu_adjustment_chains_match_the_cpu() {
|
||||
for depth in [BitDepth::Bpc8, BitDepth::Bpc16, BitDepth::Bpc32] {
|
||||
for masked in [false, true] {
|
||||
let s = adjustment_scene(depth, &gpu_stack(), masked);
|
||||
check(&format!("gpu adjustment {depth:?} masked {masked}"), compare_at(&s, opts(), Tick::ZERO), 0.0);
|
||||
check(&format!("gpu adjustment {depth:?} masked {masked} half"), compare_at(&s, RenderOpts { scale: 0.5, ..opts() }, Tick::ZERO), 0.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixed_adjustment_chains_fall_back_per_effect() {
|
||||
let mut stack = gpu_stack();
|
||||
stack.insert(1, ("ec.stylize.posterize", vec![]));
|
||||
stack.push(("ec.stylize.glow", vec![("threshold", n(30.0))]));
|
||||
for depth in [BitDepth::Bpc8, BitDepth::Bpc32] {
|
||||
let s = adjustment_scene(depth, &stack, true);
|
||||
check(&format!("mixed adjustment {depth:?}"), compare_at(&s, opts(), Tick::ZERO), 0.0);
|
||||
}
|
||||
// Colour-managed: effects run in the working space, compositing in linear light.
|
||||
for depth in [BitDepth::Bpc8, BitDepth::Bpc32] {
|
||||
let mut s = adjustment_scene(depth, &stack, false);
|
||||
s.p.settings.working_space = Some(ColorSpace::Rec709);
|
||||
s.p.settings.blend_linear = true;
|
||||
check(&format!("colour-managed adjustment {depth:?}"), compare_at(&s, opts(), Tick::ZERO), 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Counts where each run of the stack goes.
|
||||
#[derive(Default)]
|
||||
struct Probe {
|
||||
gpu: usize,
|
||||
cpu: usize,
|
||||
}
|
||||
|
||||
impl FxTarget for Probe {
|
||||
fn gpu(&mut self, steps: &[FxStep]) -> bool {
|
||||
self.gpu += steps.len();
|
||||
true
|
||||
}
|
||||
fn cpu(&mut self, _f: &mut dyn FnMut(effectcraft_effects::Buf) -> effectcraft_effects::Buf) {
|
||||
self.cpu += 1;
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adjustment_stacks_split_into_gpu_runs_and_cpu_steps() {
|
||||
let Some(g) = gpu() else { return };
|
||||
let mut stack = gpu_stack();
|
||||
stack.insert(1, ("ec.stylize.posterize", vec![]));
|
||||
let s = adjustment_scene(BitDepth::Bpc32, &stack, false);
|
||||
let mut r = Renderer::new(&s.p, &Pattern, RenderOpts { backend: Backend::Gpu, ..opts() });
|
||||
r.accel = Some(g);
|
||||
let ctx = r.eval_ctx(s.cid, Tick::ZERO).unwrap();
|
||||
let adj = ctx.comp.layers.iter().find(|l| l.switches.adjustment).unwrap();
|
||||
let mut p = Probe::default();
|
||||
r.run_effects_on(&ctx, adj, true, &mut p);
|
||||
assert_eq!((p.gpu, p.cpu), (3, 1));
|
||||
}
|
||||
|
|
@ -0,0 +1,163 @@
|
|||
//! GPU colour-family effects 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)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn color_balance_vibrance_tritone_mixer() {
|
||||
effect_case("ec.color.colorbalance", &[("shadowRed", n(40.0)), ("midGreen", n(-30.0)), ("hiBlue", n(60.0))]);
|
||||
effect_case("ec.color.colorbalance", &[("shadowBlue", n(-50.0)), ("midRed", n(70.0)), ("hiGreen", n(25.0)), ("preserveLuminosity", on())]);
|
||||
effect_case("ec.color.vibrance", &[("vibrance", n(60.0)), ("saturation", n(-20.0))]);
|
||||
effect_case("ec.color.vibrance", &[("vibrance", n(-40.0)), ("saturation", n(35.0))]);
|
||||
effect_case("ec.color.tritone", &[("midtones", c(0.6, 0.3, 0.5)), ("shadows", c(0.0, 0.1, 0.3)), ("blend", n(30.0))]);
|
||||
effect_case("ec.color.tritone", &[("highlights", c(1.0, 0.9, 0.6))]);
|
||||
effect_case("ec.color.channelmixer", &[("rr", n(80.0)), ("rg", n(30.0)), ("gb", n(-40.0)), ("bc", n(10.0)), ("br", n(50.0))]);
|
||||
effect_case("ec.color.channelmixer", &[("rr", n(40.0)), ("rg", n(40.0)), ("rb", n(20.0)), ("rc", n(-5.0)), ("monochrome", on())]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn black_white_colorama_selective_color() {
|
||||
effect_case("ec.color.blackwhite", &[]);
|
||||
effect_case("ec.color.blackwhite", &[("reds", n(120.0)), ("blues", n(-50.0)), ("cyans", n(200.0)), ("tint", on())]);
|
||||
effect_case("ec.color.colorama", &[]);
|
||||
effect_case("ec.color.colorama", &[("inputPhase/mode", e(4)), ("inputPhase/phaseShift", n(75.0)), ("outputCycle/cycles", n(2.5)), ("blend", n(40.0))]);
|
||||
effect_case("ec.color.colorama", &[("inputPhase/mode", e(8)), ("outputCycle/cycles", n(1.3))]);
|
||||
effect_case("ec.color.selectivecolor", &[("redsCyan", n(60.0)), ("bluesYellow", n(-40.0)), ("neutralsBlack", n(20.0)), ("whitesMagenta", n(30.0))]);
|
||||
effect_case(
|
||||
"ec.color.selectivecolor",
|
||||
&[("method", e(1)), ("greensMagenta", n(50.0)), ("yellowsCyan", n(-30.0)), ("blacksYellow", n(40.0)), ("magentasBlack", n(25.0))],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lumetri() {
|
||||
effect_case(
|
||||
"ec.color.lumetri",
|
||||
&[
|
||||
("basicCorrection/whiteBalance/temperature", n(30.0)),
|
||||
("basicCorrection/whiteBalance/tint", n(-20.0)),
|
||||
("basicCorrection/tone/exposure", n(0.5)),
|
||||
("basicCorrection/tone/contrast", n(25.0)),
|
||||
("basicCorrection/tone/highlights", n(-40.0)),
|
||||
("basicCorrection/tone/shadows", n(30.0)),
|
||||
("basicCorrection/tone/whites", n(10.0)),
|
||||
("basicCorrection/tone/blacks", n(-15.0)),
|
||||
("basicCorrection/saturation", n(130.0)),
|
||||
],
|
||||
);
|
||||
effect_case(
|
||||
"ec.color.lumetri",
|
||||
&[
|
||||
("creative/lookIntensity", n(80.0)),
|
||||
("creative/adjustments/fadedFilm", n(30.0)),
|
||||
("creative/adjustments/vibrance", n(40.0)),
|
||||
("creative/adjustments/creativeSaturation", n(70.0)),
|
||||
("creative/adjustments/shadowTint", c(0.3, 0.5, 0.7)),
|
||||
("creative/adjustments/highlightTint", c(0.7, 0.55, 0.4)),
|
||||
("creative/adjustments/tintBalance", n(-30.0)),
|
||||
("curves/curveMasterMidtones", n(30.0)),
|
||||
("curves/curveRedShadows", n(-20.0)),
|
||||
("curves/curveBlueHighlights", n(40.0)),
|
||||
],
|
||||
);
|
||||
effect_case(
|
||||
"ec.color.lumetri",
|
||||
&[
|
||||
("colorWheels/shadowsWheel", c(0.4, 0.5, 0.6)),
|
||||
("colorWheels/highlightsWheel", c(0.6, 0.5, 0.45)),
|
||||
("vignette/vignetteAmount", n(-2.0)),
|
||||
("vignette/vignetteMidpoint", n(30.0)),
|
||||
("vignette/vignetteRoundness", n(40.0)),
|
||||
("creative/adjustments/sharpen", n(50.0)),
|
||||
],
|
||||
);
|
||||
effect_case(
|
||||
"ec.color.lumetri",
|
||||
&[
|
||||
("vignette/vignetteAmount", n(1.5)),
|
||||
("vignette/vignetteRoundness", n(-60.0)),
|
||||
("vignette/vignetteFeather", n(20.0)),
|
||||
("creative/adjustments/sharpen", n(-40.0)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linear_color_key() {
|
||||
effect_case("ec.key.linearcolor", &[("keyColor", c(0.6, 0.5, 0.4)), ("tolerance", n(15.0)), ("softness", n(20.0))]);
|
||||
effect_case("ec.key.linearcolor", &[("keyColor", c(0.2, 0.8, 0.3)), ("matchColors", e(1)), ("keyOperation", e(1)), ("view", e(2))]);
|
||||
effect_case("ec.key.linearcolor", &[("keyColor", c(0.7, 0.3, 0.6)), ("matchColors", e(2)), ("tolerance", n(5.0)), ("softness", n(30.0))]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_light() {
|
||||
let screen = ("screenColour", c(0.3, 0.75, 0.35));
|
||||
effect_case("ec.keying.keylight", std::slice::from_ref(&screen));
|
||||
effect_case(
|
||||
"ec.keying.keylight",
|
||||
&[
|
||||
// A low gain against a pure screen keeps the raw matte away from 0 (the screen
|
||||
// removal divides by it; near 0 it amplifies the blurs' float rounding).
|
||||
("screenColour", c(0.0, 1.0, 0.0)),
|
||||
("screenGain", n(60.0)),
|
||||
("screenBalance", n(30.0)),
|
||||
("screenPreblur", n(3.0)),
|
||||
("screenMatte/clipBlack", n(10.0)),
|
||||
("screenMatte/clipWhite", n(80.0)),
|
||||
("screenMatte/clipRollback", n(2.5)),
|
||||
("screenMatte/screenSoftness", n(3.0)),
|
||||
("screenMatte/replaceMethod", e(3)),
|
||||
("screenMatte/replaceColour", c(0.6, 0.4, 0.3)),
|
||||
("unpremultiplyResult", Value::Bool(false)),
|
||||
],
|
||||
);
|
||||
effect_case(
|
||||
"ec.keying.keylight",
|
||||
&[
|
||||
("screenColour", c(0.2, 0.4, 0.8)),
|
||||
("lockBiasesTogether", Value::Bool(false)),
|
||||
("alphaBias", c(0.5, 0.45, 0.6)),
|
||||
("despillBias", c(0.55, 0.5, 0.45)),
|
||||
("screenMatte/clipWhite", n(70.0)),
|
||||
("screenMatte/screenDespotBlack", n(2.0)),
|
||||
("screenMatte/screenDespotWhite", n(1.5)),
|
||||
("screenMatte/screenShrinkGrow", n(-1.5)),
|
||||
("screenMatte/replaceMethod", e(2)),
|
||||
("foregroundColourCorrection/enableColourCorrection", on()),
|
||||
("foregroundColourCorrection/saturation", n(120.0)),
|
||||
("foregroundColourCorrection/contrast", n(15.0)),
|
||||
("edgeColourCorrection/enableEdgeColourCorrection", on()),
|
||||
("edgeColourCorrection/edgeSoftness", n(2.0)),
|
||||
("edgeColourCorrection/edgeHardness", n(30.0)),
|
||||
("edgeColourCorrection/edgeBrightness", n(-20.0)),
|
||||
("sourceCrops/cropLeft", n(13.0)),
|
||||
("sourceCrops/cropBottom", n(7.0)),
|
||||
("insideMask/sourceAlpha", e(1)),
|
||||
],
|
||||
);
|
||||
// Soft Colour without Screen Softness (after the blur, the screen-removed foreground of
|
||||
// nearly transparent pixels, divided by a raw matte near 1e-4, amplifies float rounding).
|
||||
effect_case(
|
||||
"ec.keying.keylight",
|
||||
&[
|
||||
screen.clone(),
|
||||
("screenMatte/clipBlack", n(15.0)),
|
||||
("screenMatte/clipRollback", n(1.5)),
|
||||
("screenMatte/replaceColour", c(0.6, 0.4, 0.3)),
|
||||
("insideMask/sourceAlpha", e(0)),
|
||||
],
|
||||
);
|
||||
for view in [1, 2, 4, 7, 9] {
|
||||
effect_case("ec.keying.keylight", &[screen.clone(), ("view", e(view)), ("screenMatte/screenShrinkGrow", n(1.0)), ("screenMatte/replaceMethod", e(1))]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
//! Distort / stylize GPU effects vs the CPU effects (the oracle): see `fx_distort.rs`.
|
||||
|
||||
use effectcraft_keyframe::Value;
|
||||
use effectcraft_project::BitDepth;
|
||||
use effectcraft_render::RenderOpts;
|
||||
use effectcraft_time::Tick;
|
||||
|
||||
use crate::tests::{Scene, check, compare_at, effect_case, n, opts, set};
|
||||
|
||||
fn e(v: u32) -> Value {
|
||||
Value::Enum(v)
|
||||
}
|
||||
|
||||
fn on() -> Value {
|
||||
Value::Bool(true)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn twirl_bulge_ripple_wave_warp() {
|
||||
effect_case("ec.distort.twirl", &[("angle", n(120.0)), ("radius", n(40.0))]);
|
||||
effect_case("ec.distort.twirl", &[("angle", n(-400.0)), ("radius", n(70.0)), ("center", Value::Vec2([25.0, 18.0]))]);
|
||||
effect_case("ec.distort.bulge", &[("hradius", n(30.0)), ("vradius", n(20.0)), ("height", n(1.8))]);
|
||||
effect_case("ec.distort.bulge", &[("height", n(-2.5)), ("taperRadius", n(60.0)), ("pinAllEdges", on()), ("center", Value::Vec2([30.0, 25.0]))]);
|
||||
effect_case("ec.distort.ripple", &[("radius", n(80.0)), ("waveWidth", n(9.0)), ("waveHeight", n(4.0))]);
|
||||
effect_case("ec.distort.ripple", &[("conversion", e(1)), ("phase", n(70.0)), ("speed", n(3.0)), ("center", Value::Vec2([20.0, 30.0]))]);
|
||||
effect_case("ec.distort.wavewarp", &[("height", n(6.0)), ("width", n(25.0)), ("direction", n(30.0))]);
|
||||
effect_case("ec.distort.wavewarp", &[("waveType", e(2)), ("pinning", e(1)), ("phase", n(400.0)), ("speed", n(-2.0))]);
|
||||
effect_case("ec.distort.wavewarp", &[("waveType", e(8)), ("pinning", e(2)), ("height", n(-5.0)), ("direction", n(200.0))]);
|
||||
effect_case("ec.distort.wavewarp", &[("waveType", e(4)), ("pinning", e(7)), ("width", n(33.0))]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn turbulent_displace_and_cc_lens() {
|
||||
effect_case("ec.distort.turbulentdisplace", &[("size", n(30.0)), ("amount", n(20.0))]);
|
||||
effect_case(
|
||||
"ec.distort.turbulentdisplace",
|
||||
&[("displacement", e(1)), ("complexity", n(3.5)), ("evolution", n(90.0)), ("size", n(25.0)), ("pinning", e(2)), ("resizeLayer", on())],
|
||||
);
|
||||
effect_case(
|
||||
"ec.distort.turbulentdisplace",
|
||||
&[
|
||||
("displacement", e(5)),
|
||||
("evolutionOptions/cycleEvolution", on()),
|
||||
("evolutionOptions/cycle", n(2.0)),
|
||||
("evolution", n(500.0)),
|
||||
("evolutionOptions/randomSeed", n(7.0)),
|
||||
("pinning", e(0)),
|
||||
("amount", n(-30.0)),
|
||||
],
|
||||
);
|
||||
effect_case("ec.distort.turbulentdisplace", &[("displacement", e(8)), ("size", n(15.0)), ("pinning", e(6))]);
|
||||
effect_case("ec.distort.cclens", &[("size", n(70.0))]);
|
||||
effect_case("ec.distort.cclens", &[("size", n(45.0)), ("convergence", n(60.0)), ("center", Value::Vec2([30.0, 20.0]))]);
|
||||
effect_case("ec.distort.cclens", &[("convergence", n(-70.0))]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mesh_warp() {
|
||||
// 2 rows × 3 columns: 12 vertices.
|
||||
let mesh = "0,0 0,0 0,0 0,0 0,0 6,-4 -5,3 0,0 0,0 0,0 0,0 0,0";
|
||||
effect_case("ec.distort.meshwarp", &[("rows", n(2.0)), ("columns", n(3.0)), ("quality", n(3.0)), ("mesh", Value::Str(mesh.into()))]);
|
||||
let mesh = "-4,-3 2,0 8,5 0,-2 9,9 1,0 -3,4 2,2 4,-6";
|
||||
effect_case("ec.distort.meshwarp", &[("rows", n(2.0)), ("columns", n(2.0)), ("quality", n(5.0)), ("mesh", Value::Str(mesh.into()))]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mosaic_find_edges_emboss() {
|
||||
effect_case("ec.stylize.mosaic", &[]);
|
||||
effect_case("ec.stylize.mosaic", &[("horizontal", n(7.0)), ("vertical", n(3.0))]);
|
||||
effect_case("ec.stylize.mosaic", &[("horizontal", n(13.0)), ("vertical", n(9.0)), ("sharpColors", on())]);
|
||||
effect_case("ec.stylize.findedges", &[]);
|
||||
effect_case("ec.stylize.findedges", &[("invert", on()), ("blend", n(30.0))]);
|
||||
effect_case("ec.stylize.emboss", &[]);
|
||||
effect_case("ec.stylize.emboss", &[("direction", n(160.0)), ("relief", n(4.0)), ("contrast", n(250.0)), ("blend", n(20.0))]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn displacement_map_self() {
|
||||
effect_case("ec.distort.displacementmap", &[("maxHorizontal", n(8.0)), ("maxVertical", n(-5.0))]);
|
||||
effect_case("ec.distort.displacementmap", &[("useForHorizontal", e(4)), ("useForVertical", e(3)), ("wrapPixelsAround", on())]);
|
||||
effect_case(
|
||||
"ec.distort.displacementmap",
|
||||
&[("useForHorizontal", e(10)), ("useForVertical", e(6)), ("maxVertical", n(12.0)), ("expandOutput", Value::Bool(false))],
|
||||
);
|
||||
}
|
||||
|
||||
/// Displacement Map reading another layer (Center / Stretch / Tile), composited.
|
||||
#[test]
|
||||
fn displacement_map_layer() {
|
||||
for depth in [BitDepth::Bpc8, BitDepth::Bpc32] {
|
||||
for (behavior, wrap) in [(0, false), (1, false), (2, true), (2, false)] {
|
||||
let mut s = Scene::new(depth);
|
||||
let bg = s.solid([0.15, 0.1, 0.2], 97, 61);
|
||||
s.push(bg);
|
||||
let mut map = s.footage(50, 36);
|
||||
map.switches.video = false;
|
||||
let mid = s.push(map);
|
||||
let mut l = s.footage(70, 44);
|
||||
s.effect(
|
||||
&mut l,
|
||||
"ec.distort.displacementmap",
|
||||
&[
|
||||
("displacementMapLayer", Value::Layer(Some(mid.0))),
|
||||
("useForHorizontal", e(0)),
|
||||
("useForVertical", e(4)),
|
||||
("maxHorizontal", n(9.0)),
|
||||
("maxVertical", n(6.0)),
|
||||
("displacementMapBehavior", e(behavior)),
|
||||
("wrapPixelsAround", Value::Bool(wrap)),
|
||||
],
|
||||
);
|
||||
set(&mut l, "transform/rotation", Value::Scalar(8.0));
|
||||
s.push(l);
|
||||
let label = format!("displacement map {behavior} {wrap} {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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,200 @@
|
|||
//! GPU vs CPU for the blur, transition and generate family (`fx_generate`).
|
||||
|
||||
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 b(v: bool) -> Value {
|
||||
Value::Bool(v)
|
||||
}
|
||||
|
||||
fn pt(x: f64, y: f64) -> Value {
|
||||
Value::Vec2([x, y])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn radial_blur() {
|
||||
effect_case("ec.blur.radial", &[("amount", n(12.0))]);
|
||||
effect_case("ec.blur.radial", &[("amount", n(30.0)), ("type", e(1)), ("center", pt(20.3, 14.7))]);
|
||||
effect_case("ec.blur.radial", &[("amount", n(-45.0)), ("center", pt(50.0, 30.0))]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cc_radial_fast_blur() {
|
||||
effect_case("ec.blur.ccradialfast", &[("amount", n(40.0))]);
|
||||
effect_case("ec.blur.ccradialfast", &[("amount", n(70.0)), ("zoom", e(1)), ("center", pt(22.4, 16.1))]);
|
||||
effect_case("ec.blur.ccradialfast", &[("amount", n(25.0)), ("zoom", e(2))]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn camera_lens_blur() {
|
||||
effect_case("ec.blur.cameralens", &[("blurRadius", n(5.0))]);
|
||||
effect_case(
|
||||
"ec.blur.cameralens",
|
||||
&[
|
||||
("blurRadius", n(7.5)),
|
||||
("irisProperties/irisShape", e(2)),
|
||||
("irisProperties/irisRotation", n(20.0)),
|
||||
("irisProperties/irisAspectRatio", n(1.6)),
|
||||
("repeatEdge", b(true)),
|
||||
],
|
||||
);
|
||||
effect_case(
|
||||
"ec.blur.cameralens",
|
||||
&[
|
||||
("blurRadius", n(4.0)),
|
||||
("irisProperties/irisShape", e(8)),
|
||||
("highlight/specularBrightness", n(60.0)),
|
||||
("highlight/specularThreshold", n(150.0)),
|
||||
("highlight/highlightSaturation", n(40.0)),
|
||||
("useLinear", b(true)),
|
||||
],
|
||||
);
|
||||
// Large iris: per-row prefix sums.
|
||||
effect_case("ec.blur.cameralens", &[("blurRadius", n(14.0)), ("irisProperties/irisRoundness", n(60.0))]);
|
||||
effect_case("ec.blur.cameralens", &[("blurRadius", n(12.0)), ("repeatEdge", b(true)), ("useLinear", b(true))]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wipes() {
|
||||
effect_case("ec.transition.linearwipe", &[("completion", n(40.0)), ("angle", n(30.0)), ("feather", n(12.0))]);
|
||||
effect_case("ec.transition.linearwipe", &[("completion", n(55.0)), ("angle", n(-120.0))]);
|
||||
effect_case("ec.transition.linearwipe", &[("completion", n(20.0)), ("angle", n(90.0)), ("feather", n(3.0))]);
|
||||
effect_case("ec.transition.venetian", &[("completion", n(35.0)), ("direction", n(30.0)), ("width", n(11.0)), ("feather", n(2.0))]);
|
||||
effect_case("ec.transition.venetian", &[("completion", n(60.0)), ("width", n(17.0))]);
|
||||
effect_case("ec.transition.venetian", &[("completion", n(25.0)), ("direction", n(-70.0)), ("width", n(9.0)), ("feather", n(0.5))]);
|
||||
effect_case("ec.transition.radialwipe", &[("completion", n(30.0)), ("startAngle", n(25.0)), ("feather", n(10.0))]);
|
||||
effect_case("ec.transition.radialwipe", &[("completion", n(45.0)), ("wipe", e(1)), ("center", pt(30.2, 19.6))]);
|
||||
effect_case("ec.transition.radialwipe", &[("completion", n(60.0)), ("wipe", e(2)), ("startAngle", n(-40.0)), ("feather", n(3.0))]);
|
||||
effect_case("ec.transition.radialwipe", &[("completion", n(0.0)), ("feather", n(20.0))]);
|
||||
effect_case("ec.transition.gradientwipe", &[("completion", n(40.0))]);
|
||||
effect_case("ec.transition.gradientwipe", &[("completion", n(55.0)), ("softness", n(30.0)), ("invert", b(true))]);
|
||||
effect_case("ec.transition.gradientwipe", &[("completion", n(100.0))]);
|
||||
}
|
||||
|
||||
/// Gradient Wipe driven by another layer (fetched through the effect host, placed on the CPU).
|
||||
#[test]
|
||||
fn gradient_wipe_with_a_gradient_layer() {
|
||||
for depth in [BitDepth::Bpc8, BitDepth::Bpc32] {
|
||||
for placement in 0..3 {
|
||||
let mut s = Scene::new(depth);
|
||||
let bg = s.solid([0.15, 0.1, 0.2], 97, 61);
|
||||
s.push(bg);
|
||||
let grad = s.footage(40, 30);
|
||||
let gid = s.push(grad);
|
||||
let mut l = s.footage(70, 44);
|
||||
s.effect(
|
||||
&mut l,
|
||||
"ec.transition.gradientwipe",
|
||||
&[("completion", n(45.0)), ("softness", n(20.0)), ("gradientLayer", Value::Layer(Some(gid.0))), ("gradientPlacement", e(placement))],
|
||||
);
|
||||
set(&mut l, "transform/rotation", Value::Scalar(8.0));
|
||||
s.push(l);
|
||||
let label = format!("gradient wipe layer {placement} {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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generators() {
|
||||
effect_case("ec.generate.cellpattern", &[("size", n(14.0))]);
|
||||
effect_case("ec.generate.cellpattern", &[("cellPattern", e(2)), ("size", n(11.0)), ("evolution", n(130.0)), ("overflow", e(1)), ("contrast", n(150.0))]);
|
||||
effect_case(
|
||||
"ec.generate.cellpattern",
|
||||
&[
|
||||
("cellPattern", e(11)),
|
||||
("size", n(9.0)),
|
||||
("tilingOptions/enableTiling", b(true)),
|
||||
("tilingOptions/cellsHorizontal", n(4.0)),
|
||||
("tilingOptions/cellsVertical", n(3.0)),
|
||||
("overflow", e(2)),
|
||||
("contrast", n(250.0)),
|
||||
("invert", b(true)),
|
||||
("evolutionOptions/randomSeed", n(17.0)),
|
||||
],
|
||||
);
|
||||
effect_case("ec.generate.cellpattern", &[("cellPattern", e(5)), ("size", n(16.0)), ("disperse", n(0.6)), ("offset", pt(3.3, -7.1))]);
|
||||
effect_case("ec.generate.checkerboard", &[("width", n(9.0))]);
|
||||
effect_case(
|
||||
"ec.generate.checkerboard",
|
||||
&[
|
||||
("sizeFrom", e(2)),
|
||||
("width", n(13.0)),
|
||||
("height", n(7.0)),
|
||||
("feather/featherWidth", n(4.0)),
|
||||
("anchor", pt(5.3, 2.2)),
|
||||
("blendingMode", e(5)),
|
||||
("opacity", n(70.0)),
|
||||
],
|
||||
);
|
||||
effect_case("ec.generate.checkerboard", &[("sizeFrom", e(0)), ("corner", pt(16.0, 11.0)), ("color", c(0.9, 0.3, 0.1)), ("blendingMode", e(1))]);
|
||||
effect_case("ec.generate.grid", &[("width", n(12.0)), ("border", n(3.0))]);
|
||||
effect_case(
|
||||
"ec.generate.grid",
|
||||
&[
|
||||
("sizeFrom", e(2)),
|
||||
("width", n(15.0)),
|
||||
("height", n(9.0)),
|
||||
("border", n(2.5)),
|
||||
("feather/featherWidth", n(3.0)),
|
||||
("feather/featherHeight", n(2.0)),
|
||||
("invertGrid", b(true)),
|
||||
("blendingMode", e(4)),
|
||||
],
|
||||
);
|
||||
effect_case(
|
||||
"ec.generate.grid",
|
||||
&[("sizeFrom", e(0)), ("corner", pt(11.0, 8.0)), ("anchor", pt(2.5, 3.5)), ("blendingMode", e(14)), ("color", c(0.2, 0.8, 0.4))],
|
||||
);
|
||||
effect_case("ec.generate.fourcolor", &[]);
|
||||
effect_case("ec.generate.fourcolor", &[("blend", n(40.0)), ("jitter", n(50.0)), ("opacity", n(80.0)), ("blendingMode", e(3))]);
|
||||
effect_case("ec.generate.fourcolor", &[("blend", n(600.0)), ("blendingMode", e(16)), ("positionsColors/point2", pt(30.0, 20.0))]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn noise_and_grain() {
|
||||
effect_case("ec.noise.noise", &[("amount", n(30.0))]);
|
||||
effect_case("ec.noise.noise", &[("amount", n(50.0)), ("color", b(false))]);
|
||||
effect_case("ec.noise.noise", &[("amount", n(40.0)), ("clip", b(false))]);
|
||||
effect_case("ec.noise.addgrain", &[("tweaking/intensity", n(2.0))]);
|
||||
effect_case(
|
||||
"ec.noise.addgrain",
|
||||
&[
|
||||
("tweaking/intensity", n(3.0)),
|
||||
("tweaking/size", n(2.5)),
|
||||
("tweaking/softness", n(1.5)),
|
||||
("tweaking/aspectRatio", n(1.4)),
|
||||
("tweaking/channelSize/redSize", n(0.6)),
|
||||
("color/saturation", n(0.5)),
|
||||
("color/tintAmount", n(0.4)),
|
||||
("color/tintColor", c(1.0, 0.6, 0.2)),
|
||||
("application/blendingMode", e(4)),
|
||||
],
|
||||
);
|
||||
effect_case(
|
||||
"ec.noise.addgrain",
|
||||
&[
|
||||
("tweaking/intensity", n(1.5)),
|
||||
("color/monochromatic", b(true)),
|
||||
("application/blendingMode", e(3)),
|
||||
("application/shadows", n(0.3)),
|
||||
("application/highlights", n(1.8)),
|
||||
("application/midpoint", n(0.35)),
|
||||
("animation/randomSeed", n(42.0)),
|
||||
("animation/animationSpeed", n(2.0)),
|
||||
],
|
||||
);
|
||||
effect_case(
|
||||
"ec.noise.addgrain",
|
||||
&[("tweaking/intensity", n(2.0)), ("application/blendingMode", e(1)), ("tweaking/channelIntensities/blueIntensity", n(2.5))],
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
//! GPU particles vs the CPU simulation (the oracle): CC Particle World, CC Particle Systems II
|
||||
//! and Particle Playground's cannon at fixed seeds must give the same live particles (same
|
||||
//! ids) at positions within tolerance, from scratch and when advancing / seeking from GPU
|
||||
//! checkpoints.
|
||||
|
||||
use effectcraft_effects::psim::{ParticleSim, SimParticle};
|
||||
use effectcraft_effects::{EffectCtx, EffectEnv, EffectHost, LayerPixels, Params};
|
||||
use effectcraft_keyframe::Value;
|
||||
|
||||
use crate::Gpu;
|
||||
use crate::tests::{gpu, n};
|
||||
|
||||
struct Host(&'static Gpu);
|
||||
|
||||
impl EffectHost for Host {
|
||||
fn layer(&self, _id: u64, _masks_and_effects: bool) -> Option<LayerPixels> {
|
||||
None
|
||||
}
|
||||
fn audio(&self, _id: u64, _start: f64, _frames: usize, _rate: u32) -> Option<Vec<f32>> {
|
||||
None
|
||||
}
|
||||
fn particles(&self) -> Option<&dyn ParticleSim> {
|
||||
Some(self.0)
|
||||
}
|
||||
}
|
||||
|
||||
fn params(id: &str, vals: &[(&str, Value)]) -> Params {
|
||||
let spec = effectcraft_effects::find(id).unwrap();
|
||||
let size = [320.0, 240.0];
|
||||
let mut p = Params { values: spec.params.iter().map(|ps| (ps.id.to_string(), effectcraft_effects::default_value(ps, size))).collect() };
|
||||
for (k, v) in vals {
|
||||
assert!(p.values.contains_key(*k), "{id}: no {k}");
|
||||
p.values.insert(k.to_string(), v.clone());
|
||||
}
|
||||
p
|
||||
}
|
||||
|
||||
/// (CPU, GPU) particles of effect `id` at layer time `t`.
|
||||
fn states(id: &str, p: &Params, t: f64, host: &Host) -> (Vec<SimParticle>, Vec<SimParticle>) {
|
||||
let ctx = |env| EffectCtx { params: p, time: t, layer_size: [320.0, 240.0], seed: 5, adjustment: false, env };
|
||||
let run =
|
||||
|c: &EffectCtx| if id == "ec.sim.particleplayground" { effectcraft_effects::playground_state(c) } else { effectcraft_effects::particle_state(id, c) };
|
||||
let cpu = run(&ctx(EffectEnv::default())).expect("cpu state");
|
||||
let gpu = run(&ctx(EffectEnv { host: Some(host), ..Default::default() })).expect("gpu state");
|
||||
(cpu, gpu)
|
||||
}
|
||||
|
||||
/// Same ids alive (up to `id_slack` particles dying a step apart), positions within `tol`
|
||||
/// (world units) for 99 % of them and on average within `tol / 10`.
|
||||
fn agree(label: &str, cpu: &[SimParticle], gpu: &[SimParticle], tol: f32, id_slack: usize) {
|
||||
assert!(cpu.len() > 20, "{label}: the scene has particles ({})", cpu.len());
|
||||
let gi: std::collections::HashMap<u32, &SimParticle> = gpu.iter().map(|q| (q.id, q)).collect();
|
||||
let mut missing = 0;
|
||||
let mut errs = vec![];
|
||||
for q in cpu {
|
||||
match gi.get(&q.id) {
|
||||
Some(g) => errs.push((0..3).map(|k| (q.p[k] - g.p[k]).abs()).fold(0.0f32, f32::max)),
|
||||
None => missing += 1,
|
||||
}
|
||||
}
|
||||
let extra = gpu.len() + missing - cpu.len();
|
||||
assert!(missing + extra <= id_slack, "{label}: {missing} CPU particles missing on the GPU, {extra} extra (of {})", cpu.len());
|
||||
errs.sort_by(f32::total_cmp);
|
||||
let mean = errs.iter().sum::<f32>() / errs.len() as f32;
|
||||
let p99 = errs[(errs.len() * 99 / 100).min(errs.len() - 1)];
|
||||
eprintln!("{label}: {} particles, mean err {mean:.2e}, p99 {p99:.2e}, max {:.2e}", cpu.len(), errs.last().unwrap());
|
||||
assert!(p99 <= tol && mean <= tol / 10.0, "{label}: positions differ (mean {mean}, p99 {p99})");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn particle_world_matches_the_cpu() {
|
||||
let Some(g) = gpu() else { return };
|
||||
let host = Host(g);
|
||||
for anim in [0u32, 1, 2, 3, 4, 5, 6, 7, 8, 9] {
|
||||
let p = params(
|
||||
"ec.sim.ccparticleworld",
|
||||
&[
|
||||
("physics/animation", Value::Enum(anim)),
|
||||
("birthRate", n(2.0)),
|
||||
("longevity", n(1.2)),
|
||||
("physics/extra", n(0.6)),
|
||||
("physics/resistance", n(0.5)),
|
||||
("extras/randomSeed", n(7.0)),
|
||||
],
|
||||
);
|
||||
let (cpu, gpu) = states("ec.sim.ccparticleworld", &p, 1.5, &host);
|
||||
// Fractal / fire animations sample value noise along the path: rounding differences can
|
||||
// grow, so they get a looser bound.
|
||||
let tol = if matches!(anim, 6 | 8 | 9) { 1e-3 } else { 1e-4 };
|
||||
agree(&format!("particle world anim {anim}"), &cpu, &gpu, tol, cpu.len() / 100 + 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn particle_systems_ii_matches_the_cpu() {
|
||||
let Some(g) = gpu() else { return };
|
||||
let host = Host(g);
|
||||
for anim in [0u32, 1, 2, 4, 5, 8] {
|
||||
let p = params(
|
||||
"ec.sim.ccparticlesystems2",
|
||||
&[("physics/animation", Value::Enum(anim)), ("birthRate", n(3.0)), ("longevity", n(1.5)), ("physics/gravity", n(0.8)), ("randomSeed", n(3.0))],
|
||||
);
|
||||
let (cpu, gpu) = states("ec.sim.ccparticlesystems2", &p, 2.0, &host);
|
||||
let tol = if anim == 8 { 1e-3 } else { 1e-4 };
|
||||
agree(&format!("particle systems ii anim {anim}"), &cpu, &gpu, tol, cpu.len() / 100 + 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn playground_cannon_matches_the_cpu_and_checkpoints_reuse() {
|
||||
let Some(g) = gpu() else { return };
|
||||
let host = Host(g);
|
||||
for (radius, spread) in [(4.0, 0.0), (-10.0, 50.0)] {
|
||||
let p = params(
|
||||
"ec.sim.particleplayground",
|
||||
&[
|
||||
("grid/particlesAcross", n(0.0)),
|
||||
("cannon/barrelRadius", n(radius)),
|
||||
("cannon/particlesPerSecond", n(90.0)),
|
||||
("cannon/directionRandomSpread", n(30.0)),
|
||||
("cannon/velocityRandomSpread", n(40.0)),
|
||||
("gravity/gravityForceRandomSpread", n(spread)),
|
||||
],
|
||||
);
|
||||
// Advance through checkpoints (0.5 → 1.0 → 2.0), seek back (1.5), compare each.
|
||||
for t in [0.5, 1.0, 2.0, 1.5] {
|
||||
let (cpu, gpu) = states("ec.sim.particleplayground", &p, t, &host);
|
||||
// Positions are layer pixels here.
|
||||
agree(&format!("cannon r {radius} spread {spread} t {t}"), &cpu, &gpu, 0.01, cpu.len() / 100 + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Through the renderer: the GPU backend's effect host hands the simulation to the GPU, and
|
||||
/// the composited frames agree with the CPU render.
|
||||
#[test]
|
||||
fn particle_effects_render_like_the_cpu() {
|
||||
use effectcraft_project::BitDepth;
|
||||
use effectcraft_time::Tick;
|
||||
|
||||
use crate::tests::{Scene, check, compare_at, opts};
|
||||
for (id, vals) in [
|
||||
("ec.sim.ccparticleworld", vec![("birthRate", n(1.0)), ("physics/animation", Value::Enum(4))]),
|
||||
("ec.sim.ccparticlesystems2", vec![("birthRate", n(2.0))]),
|
||||
("ec.sim.particleplayground", vec![("grid/particlesAcross", n(0.0)), ("cannon/particlesPerSecond", n(60.0))]),
|
||||
] {
|
||||
for depth in [BitDepth::Bpc8, BitDepth::Bpc32] {
|
||||
let mut s = Scene::new(depth);
|
||||
let mut l = s.solid([0.1, 0.1, 0.15], 97, 61);
|
||||
s.effect(&mut l, id, &vals);
|
||||
s.push(l);
|
||||
check(&format!("{id} {depth:?}"), compare_at(&s, opts(), Tick::from_seconds_f64(1.0)), 0.002);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +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. Steps without a GPU implementation (3D runs, adjustment and
|
||||
//! wireframe layers) read the canvas back, run on the CPU and upload the result.
|
||||
//! 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.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
|
|
@ -77,7 +78,7 @@ fn draw_comp<'a>(e: &mut Enc, r: &Renderer<'a>, ctx: &EvalCtx<'a>, canvas: &mut
|
|||
while j < visible.len() && visible[j].is_3d() {
|
||||
j += 1;
|
||||
}
|
||||
on_cpu(e, canvas, |img| r.draw_3d_run(ctx, &visible[i..j], img))?;
|
||||
draw_3d(e, r, ctx, &visible[i..j], canvas)?;
|
||||
i = j;
|
||||
} else {
|
||||
match r.collapsed(ctx, visible[i]) {
|
||||
|
|
@ -94,6 +95,25 @@ fn draw_comp<'a>(e: &mut Enc, r: &Renderer<'a>, ctx: &EvalCtx<'a>, canvas: &mut
|
|||
Some(())
|
||||
}
|
||||
|
||||
/// A run of consecutive 3D layers: Classic 3D on the GPU (`classic3d`), with 3D adjustment
|
||||
/// 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<()> {
|
||||
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)?;
|
||||
return draw_3d(e, r, ctx, &run[k + 1..], canvas);
|
||||
}
|
||||
if run.is_empty() {
|
||||
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))?,
|
||||
}
|
||||
Some(())
|
||||
}
|
||||
|
||||
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 {
|
||||
|
|
@ -113,9 +133,14 @@ fn draw_collapsed<'a>(e: &mut Enc, r: &Renderer<'a>, ctx: &EvalCtx<'a>, layer: &
|
|||
}
|
||||
|
||||
fn draw_layer(e: &mut Enc, r: &Renderer, ctx: &EvalCtx, layer: &Layer, canvas: &mut GpuImage) -> Option<()> {
|
||||
if layer.switches.adjustment || layer.switches.quality == Quality::Wireframe {
|
||||
// Adjustment layers run their effects on the comp below (CPU, with GPU effects where
|
||||
// available); wireframes draw outlines.
|
||||
if layer.switches.adjustment {
|
||||
if draw_adjustment(e, r, ctx, layer, canvas).is_some() {
|
||||
return Some(());
|
||||
}
|
||||
return on_cpu(e, canvas, |img| r.draw_layer_cpu(ctx, layer, img));
|
||||
}
|
||||
if layer.switches.quality == Quality::Wireframe {
|
||||
// Wireframes draw outlines on the CPU.
|
||||
return on_cpu(e, canvas, |img| r.draw_layer_cpu(ctx, layer, img));
|
||||
}
|
||||
let opacity = r.layer_opacity(ctx, layer);
|
||||
|
|
@ -151,6 +176,33 @@ fn draw_layer(e: &mut Enc, r: &Renderer, ctx: &EvalCtx, layer: &Layer, canvas: &
|
|||
Some(())
|
||||
}
|
||||
|
||||
/// Adjustment layer (`Renderer::draw_adjustment`): the effect stack runs on the comp below,
|
||||
/// resident on the GPU (non-GPU effects read back, run on the CPU and upload), and the result
|
||||
/// replaces the canvas inside the layer's footprint × opacity. `None` = draw it on the CPU.
|
||||
fn draw_adjustment(e: &mut Enc, r: &Renderer, ctx: &EvalCtx, layer: &Layer, canvas: &mut GpuImage) -> Option<()> {
|
||||
let opacity = r.layer_opacity(ctx, layer);
|
||||
let Some(foot) = r.adjustment_footprint(ctx, layer) else { return Some(()) };
|
||||
let (w, h) = (canvas.width, canvas.height);
|
||||
let matte = place(e, r, ctx, layer, &Arc::new(foot), &e.image(w, h), BlendMode::Normal, 1.0)?;
|
||||
let pipe = r.pipe();
|
||||
// Effects run in the working space, not the linear blending space.
|
||||
let below = match pipe.from_blend() {
|
||||
Some(c) => ops::convert(e, canvas, &c),
|
||||
None => canvas.clone(),
|
||||
};
|
||||
let mut fx = crate::effects::GpuFx::new(e, below, [0.0; 2], r.opts.scale, pipe.levels);
|
||||
r.run_effects_on(ctx, layer, true, &mut fx);
|
||||
let (mut adjusted, _, _) = fx.finish()?;
|
||||
if adjusted.width != w || adjusted.height != h {
|
||||
return Some(());
|
||||
}
|
||||
if let Some(c) = pipe.to_blend() {
|
||||
adjusted = ops::convert(e, &adjusted, &c);
|
||||
}
|
||||
*canvas = ops::adjust_mix(e, canvas, &adjusted, &matte, opacity);
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn composite_layer(e: &mut Enc, r: &Renderer, ctx: &EvalCtx, layer: &Layer, buf: &Arc<Buf>, canvas: &mut GpuImage, opacity: f32) -> Option<()> {
|
||||
let matte = r.track_matte(ctx, layer).is_some();
|
||||
if !matte && !layer.preserve_transparency {
|
||||
|
|
|
|||
|
|
@ -82,6 +82,9 @@ struct FxHost<'r, 'a, 'c> {
|
|||
const MAX_FX_DEPTH: usize = 8;
|
||||
|
||||
impl EffectHost for FxHost<'_, '_, '_> {
|
||||
fn particles(&self) -> Option<&dyn effectcraft_effects::psim::ParticleSim> {
|
||||
self.r.active_accel().and_then(|a| a.particles())
|
||||
}
|
||||
fn layer(&self, id: u64, masks_and_effects: bool) -> Option<LayerPixels> {
|
||||
let other = self.ctx.layer(effectcraft_project::LayerId(id))?;
|
||||
if other.id == self.layer.id || self.r.depth > MAX_FX_DEPTH {
|
||||
|
|
@ -240,6 +243,46 @@ pub trait Accelerator: Send + Sync {
|
|||
fn raster_3d(&self, _scene: &three_d::adv::Scene) -> Option<three_d::adv::Target> {
|
||||
None
|
||||
}
|
||||
/// A particle simulation backend (GPU particles) for the stepped particle effects, with
|
||||
/// the CPU simulation's semantics. `None` = they simulate on the CPU.
|
||||
fn particles(&self) -> Option<&dyn effectcraft_effects::psim::ParticleSim> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Where an effect stack runs (see [`Renderer::run_effects_on`]): a CPU buffer, or an image
|
||||
/// resident on an accelerator.
|
||||
pub trait FxTarget {
|
||||
/// Run a chain of accelerator-supported effects (quantising after each to the bit depth).
|
||||
/// `false` = not handled: the effects then run one by one through [`FxTarget::cpu`].
|
||||
fn gpu(&mut self, steps: &[FxStep]) -> bool;
|
||||
/// Run one CPU effect: `f` maps the buffer.
|
||||
fn cpu(&mut self, f: &mut dyn FnMut(Buf) -> Buf);
|
||||
}
|
||||
|
||||
/// The CPU effect target (GPU chains go through [`Accelerator::effects`]: upload, readback).
|
||||
struct CpuFx<'a> {
|
||||
accel: Option<&'a dyn Accelerator>,
|
||||
levels: Option<f32>,
|
||||
buf: Option<Buf>,
|
||||
}
|
||||
|
||||
impl FxTarget for CpuFx<'_> {
|
||||
fn gpu(&mut self, steps: &[FxStep]) -> bool {
|
||||
let (Some(a), Some(b)) = (self.accel, &self.buf) else { return false };
|
||||
match a.effects(steps, b, self.levels) {
|
||||
Some(out) => {
|
||||
self.buf = Some(out);
|
||||
true
|
||||
}
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
fn cpu(&mut self, f: &mut dyn FnMut(Buf) -> Buf) {
|
||||
if let Some(b) = self.buf.take() {
|
||||
self.buf = Some(f(b));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
|
|
@ -494,11 +537,8 @@ impl<'a> Renderer<'a> {
|
|||
/// Render a composition at comp time `t` (transparent background, comp size × scale).
|
||||
/// Top-level frames go to the GPU when [`Self::active_accel`] is set and handles them.
|
||||
pub fn comp_frame(&self, comp_id: ItemId, t: Tick) -> Image {
|
||||
// Auto keeps comps with 3D layers on the CPU compositor: 3D runs are still composited on
|
||||
// the CPU, and the GPU path's readback/upload around them makes such comps slower.
|
||||
let auto_3d = self.opts.backend == Backend::Auto && self.project.comp(comp_id).is_some_and(|c| c.has_3d());
|
||||
// Classic 3D runs composite on the GPU too (M12.7), so Auto sends 3D comps there as well.
|
||||
if self.depth == 0
|
||||
&& !auto_3d
|
||||
&& let Some(a) = self.active_accel()
|
||||
&& let Some(img) = a.comp_frame(self, comp_id, t)
|
||||
{
|
||||
|
|
@ -601,19 +641,28 @@ impl<'a> Renderer<'a> {
|
|||
}
|
||||
|
||||
/// Run the first `limit` effects of the layer's stack (by position; disabled ones count).
|
||||
fn apply_effects_timed(
|
||||
fn apply_effects_timed(&self, ctx: &EvalCtx, layer: &Layer, buf: Buf, adjustment: bool, limit: usize, timing: Option<&mut Vec<(String, f64)>>) -> Buf {
|
||||
let mut t = CpuFx { accel: self.active_accel(), levels: self.pipe.levels, buf: Some(buf) };
|
||||
self.run_effect_stack(ctx, layer, adjustment, limit, timing, &mut t);
|
||||
t.buf.unwrap_or_else(|| Buf { img: Image::new(1, 1), offset: [0.0; 2], scale: self.opts.scale })
|
||||
}
|
||||
|
||||
/// Run the layer's video effect stack on `target`: runs of GPU-capable effects go to
|
||||
/// [`FxTarget::gpu`] (when an accelerator is active and supports them), the rest to
|
||||
/// [`FxTarget::cpu`] one by one (quantised to the bit depth after each).
|
||||
fn run_effect_stack(
|
||||
&self,
|
||||
ctx: &EvalCtx,
|
||||
layer: &Layer,
|
||||
mut buf: Buf,
|
||||
adjustment: bool,
|
||||
limit: usize,
|
||||
mut timing: Option<&mut Vec<(String, f64)>>,
|
||||
) -> Buf {
|
||||
target: &mut dyn FxTarget,
|
||||
) {
|
||||
if !layer.switches.effects {
|
||||
return buf;
|
||||
return;
|
||||
}
|
||||
let Some(fx) = layer.effects() else { return buf };
|
||||
let Some(fx) = layer.effects() else { return };
|
||||
let lt = layer.layer_time(ctx.time);
|
||||
let size = source_size(self.project, layer);
|
||||
let layer_size = if size.0 == 0 { [ctx.comp.width as f64, ctx.comp.height as f64] } else { [size.0 as f64, size.1 as f64] };
|
||||
|
|
@ -644,7 +693,7 @@ impl<'a> Renderer<'a> {
|
|||
let mut k = 0;
|
||||
while k < stack.len() {
|
||||
let (i, g, spec) = stack[k];
|
||||
// A run of GPU effects goes to the accelerator in one upload / readback.
|
||||
// A run of GPU effects goes to the accelerator in one go.
|
||||
if let Some(a) = accel
|
||||
&& spec.gpu
|
||||
&& a.supports_effect(spec.id)
|
||||
|
|
@ -660,8 +709,7 @@ impl<'a> Renderer<'a> {
|
|||
})
|
||||
.collect();
|
||||
let t0 = web_time::Instant::now();
|
||||
if let Some(out) = a.effects(&steps, &buf, self.pipe.levels) {
|
||||
buf = out;
|
||||
if target.gpu(&steps) {
|
||||
if let Some(v) = timing.as_deref_mut() {
|
||||
let ms = t0.elapsed().as_secs_f64() * 1e3 / run.len() as f64;
|
||||
v.extend(run.iter().map(|(_, _, s)| (format!("{} (gpu)", s.id), ms)));
|
||||
|
|
@ -676,14 +724,16 @@ impl<'a> Renderer<'a> {
|
|||
let env = EffectEnv { effect_index: i, ..env };
|
||||
let ectx = EffectCtx { params: ¶ms, time: lt.seconds(), layer_size, seed: g.uid as u32, adjustment, env };
|
||||
let t0 = web_time::Instant::now();
|
||||
buf = effectcraft_effects::apply(spec, &ectx, buf);
|
||||
// 8/16 bpc effects write integer pixels.
|
||||
self.pipe.quantize(&mut buf.img);
|
||||
target.cpu(&mut |buf| {
|
||||
let mut buf = effectcraft_effects::apply(spec, &ectx, buf);
|
||||
// 8/16 bpc effects write integer pixels.
|
||||
self.pipe.quantize(&mut buf.img);
|
||||
buf
|
||||
});
|
||||
if let Some(v) = timing.as_deref_mut() {
|
||||
v.push((spec.id.to_string(), t0.elapsed().as_secs_f64() * 1e3));
|
||||
}
|
||||
}
|
||||
buf
|
||||
}
|
||||
|
||||
/// Source pixels of a layer in layer space (before masks/effects).
|
||||
|
|
@ -1403,6 +1453,29 @@ impl<'a> Renderer<'a> {
|
|||
three_d::compose::draw_run(self, ctx, run, canvas);
|
||||
}
|
||||
|
||||
/// A run of consecutive 3D layers prepared for an accelerator (planes, lights, shadow
|
||||
/// casters; see [`three_d::Run3d`]) at output size `out`. `None` = draw it with
|
||||
/// [`Self::draw_3d_run`] (adjustment or wireframe layers in the run, Advanced 3D).
|
||||
pub fn prepare_3d_run(&self, ctx: &EvalCtx<'a>, run: &[&Layer], out: (u32, u32)) -> Option<three_d::Run3d> {
|
||||
three_d::compose::gpu_run(self, ctx, run, out)
|
||||
}
|
||||
|
||||
/// Run the layer's effect stack on `target` (an accelerator's resident image): GPU-capable
|
||||
/// runs through [`FxTarget::gpu`], the others through [`FxTarget::cpu`], with the CPU's
|
||||
/// parameters and environment. `adjustment`: the stack runs on the comp below (adjustment
|
||||
/// layers).
|
||||
pub fn run_effects_on(&self, ctx: &EvalCtx, layer: &Layer, adjustment: bool, target: &mut dyn FxTarget) {
|
||||
self.run_effect_stack(ctx, layer, adjustment, usize::MAX, None, target);
|
||||
}
|
||||
|
||||
/// An adjustment layer's footprint (its source with masks applied, layer space): where
|
||||
/// its effects show. `None` = the layer changes nothing.
|
||||
pub fn adjustment_footprint(&self, ctx: &EvalCtx, layer: &Layer) -> Option<Buf> {
|
||||
let mut foot = self.source(ctx, layer)?;
|
||||
let _ = masks::apply(ctx, layer, &mut foot);
|
||||
Some(foot)
|
||||
}
|
||||
|
||||
/// Draw one 2D layer into `canvas` on the CPU (any kind: adjustment, wireframe, styled…).
|
||||
pub fn draw_layer_cpu(&self, ctx: &EvalCtx, layer: &Layer, canvas: &mut Image) {
|
||||
self.draw_layer(ctx, layer, canvas, false);
|
||||
|
|
|
|||
|
|
@ -390,7 +390,7 @@ impl crate::Accelerator for ProbeAccel {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn auto_backend_keeps_3d_comps_on_the_cpu() {
|
||||
fn auto_backend_sends_2d_and_3d_comps_to_the_gpu() {
|
||||
let (mut p, cid, comp) = setup();
|
||||
p.settings.gpu_acceleration = true;
|
||||
let l = solid(&mut p, &comp, [1.0, 0.0, 0.0], 50, 50);
|
||||
|
|
@ -404,8 +404,11 @@ fn auto_backend_keeps_3d_comps_on_the_cpu() {
|
|||
};
|
||||
// 2D comp: Auto uses the GPU compositor.
|
||||
assert!(asked(&p, crate::Backend::Auto));
|
||||
// A 3D layer: Auto stays on the CPU; an explicit Gpu request still goes to the GPU.
|
||||
// A 3D layer: Classic 3D runs on the GPU too (M12.7), so Auto still asks it.
|
||||
p.comp_mut(cid).unwrap().layers[0].switches.three_d = true;
|
||||
assert!(!asked(&p, crate::Backend::Auto));
|
||||
assert!(asked(&p, crate::Backend::Auto));
|
||||
assert!(asked(&p, crate::Backend::Gpu));
|
||||
// Software Only: never.
|
||||
p.settings.gpu_acceleration = false;
|
||||
assert!(!asked(&p, crate::Backend::Auto));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,16 +29,18 @@ use crate::{EvalCtx, Renderer};
|
|||
|
||||
/// One position of a plane (several with motion blur).
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(crate) struct Geo {
|
||||
pub struct Geo {
|
||||
/// Output pixel → homogeneous layer coordinates.
|
||||
hinv: Mat3,
|
||||
pub hinv: Mat3,
|
||||
/// Camera-space depth as a function of layer (u, v): `d0·u + d1·v + d2`.
|
||||
depth: [f64; 3],
|
||||
world: Mat4,
|
||||
normal: Vec3,
|
||||
pub depth: [f64; 3],
|
||||
/// Layer → world.
|
||||
pub world: Mat4,
|
||||
/// Unit world normal.
|
||||
pub normal: Vec3,
|
||||
/// Output pixel bounds [x0, y0, x1, y1) covered by the plane.
|
||||
bbox: [i64; 4],
|
||||
cam: CameraState,
|
||||
pub bbox: [i64; 4],
|
||||
pub cam: CameraState,
|
||||
}
|
||||
|
||||
/// A prepared 3D layer.
|
||||
|
|
@ -72,7 +74,7 @@ impl Item<'_> {
|
|||
}
|
||||
|
||||
/// Build the plane geometry of a layer for a camera.
|
||||
fn geo(cam: &CameraState, world: Mat4, buf: &Buf, comp: (f64, f64), scale: f64, out: (u32, u32)) -> Option<Geo> {
|
||||
fn geo(cam: &CameraState, world: Mat4, b: [f64; 4], comp: (f64, f64), scale: f64, out: (u32, u32)) -> Option<Geo> {
|
||||
let proj = Mat4::scale(vec3(scale, scale, 1.0)) * cam.projection(comp.0, comp.1);
|
||||
let full = proj * world;
|
||||
let h = full.plane_to_mat3();
|
||||
|
|
@ -89,7 +91,6 @@ fn geo(cam: &CameraState, world: Mat4, buf: &Buf, comp: (f64, f64), scale: f64,
|
|||
return None;
|
||||
}
|
||||
let normal = normal.normalize();
|
||||
let b = buf_bounds(buf);
|
||||
let corners = [(b[0], b[1]), (b[2], b[1]), (b[2], b[3]), (b[0], b[3])];
|
||||
let (mut x0, mut y0, mut x1, mut y1) = (f64::INFINITY, f64::INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
|
||||
let mut behind = 0;
|
||||
|
|
@ -211,9 +212,10 @@ fn prepare_with<'a>(
|
|||
if let Some(dof) = cam.dof.filter(|_| !ctx.comp.draft_3d && !r.opts.draft) {
|
||||
buf = dof_blur(&cam, &dof, world, buf);
|
||||
}
|
||||
let bounds = buf_bounds(&buf);
|
||||
let n = if in_run { mb_samples(r, ctx, layer) } else { 1 };
|
||||
let geos: Vec<Geo> = if n <= 1 {
|
||||
geo(&cam, world, &buf, comp, s, out).into_iter().collect()
|
||||
geo(&cam, world, bounds, comp, s, out).into_iter().collect()
|
||||
} else {
|
||||
let fd = ctx.comp.frame_duration().seconds();
|
||||
let angle = ctx.comp.shutter_angle / 360.0;
|
||||
|
|
@ -223,7 +225,7 @@ fn prepare_with<'a>(
|
|||
let f = phase + angle * i as f64 / (n - 1) as f64;
|
||||
let sub = ctx.at(ctx.time + Tick::from_seconds_f64(f * fd));
|
||||
let c = camera_for(r, &sub);
|
||||
geo(&c, outer * sub.world_matrix(layer) * local, &buf, comp, s, out)
|
||||
geo(&c, outer * sub.world_matrix(layer) * local, bounds, comp, s, out)
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
|
|
@ -235,7 +237,6 @@ fn prepare_with<'a>(
|
|||
bbox = [bbox[0].min(g.bbox[0]), bbox[1].min(g.bbox[1]), bbox[2].max(g.bbox[2]), bbox[3].max(g.bbox[3])];
|
||||
}
|
||||
let mat = Material::of(ctx, layer);
|
||||
let bounds = buf_bounds(&buf);
|
||||
Some(Item {
|
||||
layer,
|
||||
order,
|
||||
|
|
@ -364,6 +365,23 @@ pub(crate) fn draw_run(r: &Renderer, ctx: &EvalCtx, run: &[&Layer], canvas: &mut
|
|||
return;
|
||||
}
|
||||
let out = (canvas.width, canvas.height);
|
||||
let g = gather(r, ctx, run, out);
|
||||
let casters: Vec<&Item> = g.items.iter().chain(g.extra.iter()).filter(|i| i.mat.casts_shadows != 0 && !i.geos.is_empty()).collect();
|
||||
composite(canvas, &g.items, &g.lights, &casters, run[0].id.0 as u32);
|
||||
for l in run.iter().filter(|l| l.switches.quality == effectcraft_project::Quality::Wireframe) {
|
||||
r.draw_layer(ctx, l, canvas, false);
|
||||
}
|
||||
}
|
||||
|
||||
/// The prepared planes of a run: drawn items (stack order), shadow casters outside the run, and
|
||||
/// the lights.
|
||||
struct Gathered<'a> {
|
||||
items: Vec<Item<'a>>,
|
||||
extra: Vec<Item<'a>>,
|
||||
lights: Vec<LightState>,
|
||||
}
|
||||
|
||||
fn gather<'a>(r: &Renderer<'a>, ctx: &EvalCtx<'a>, run: &[&'a Layer], out: (u32, u32)) -> Gathered<'a> {
|
||||
// A collapsed precomp's 3D layers are lit by the outer comp's lights.
|
||||
let lights = scene_lights(&r.collapse3d.map_or(*ctx, |c| c.parent));
|
||||
let mut items: Vec<Item> = run
|
||||
|
|
@ -400,11 +418,7 @@ pub(crate) fn draw_run(r: &Renderer, ctx: &EvalCtx, run: &[&Layer], canvas: &mut
|
|||
.collect();
|
||||
}
|
||||
items.sort_by_key(|i| i.order);
|
||||
let casters: Vec<&Item> = items.iter().chain(extra.iter()).filter(|i| i.mat.casts_shadows != 0 && !i.geos.is_empty()).collect();
|
||||
composite(canvas, &items, &lights, &casters, run[0].id.0 as u32);
|
||||
for l in run.iter().filter(|l| l.switches.quality == effectcraft_project::Quality::Wireframe) {
|
||||
r.draw_layer(ctx, l, canvas, false);
|
||||
}
|
||||
Gathered { items, extra, lights }
|
||||
}
|
||||
|
||||
/// A collapsed 3D precomp layer inside a 3D run: its nested layers become planes in this comp's
|
||||
|
|
@ -624,6 +638,83 @@ fn occlusion(c: &Item, p: Vec3, l: &LightState) -> [f32; 3] {
|
|||
out
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ accelerator export
|
||||
|
||||
/// A Classic 3D run prepared for an accelerator ([`Renderer::prepare_3d_run`]): every plane
|
||||
/// with its geometry, material and buffer, ready for the per-pixel depth sort, lighting and
|
||||
/// ray-cast shadows of [`draw_run`] on another device.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Run3d {
|
||||
/// Drawn planes in stack order, then shadow casters from outside the run (`draw` false).
|
||||
pub planes: Vec<Plane3d>,
|
||||
/// Indices (into `planes`) of the shadow casters, in the CPU's order.
|
||||
pub casters: Vec<usize>,
|
||||
pub lights: Vec<LightState>,
|
||||
/// Dissolve seed of the run.
|
||||
pub seed: u32,
|
||||
}
|
||||
|
||||
/// One plane of a [`Run3d`].
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Plane3d {
|
||||
/// The layer buffer (blending space; depth of field already applied).
|
||||
pub buf: Arc<Buf>,
|
||||
pub bicubic: bool,
|
||||
/// One per motion-blur sub-sample (equal weights).
|
||||
pub geos: Vec<Geo>,
|
||||
pub mat: Material,
|
||||
pub opacity: f32,
|
||||
pub draw: bool,
|
||||
/// Screen-space track matte factor (output size, row-major).
|
||||
pub matte: Option<Arc<Vec<f32>>>,
|
||||
pub preserve: bool,
|
||||
/// World → layer, and the (blurred) buffer's layer-space bounds.
|
||||
pub winv: Mat4,
|
||||
pub bounds: [f64; 4],
|
||||
/// Output pixel bounds of all geometries.
|
||||
pub bbox: [i64; 4],
|
||||
/// Stack order (coplanar fragments draw lower orders first).
|
||||
pub order: usize,
|
||||
pub layer_id: u32,
|
||||
pub mode: BlendMode,
|
||||
}
|
||||
|
||||
/// Prepare a run of 3D layers for an accelerator. `None` when the run must be drawn on the CPU
|
||||
/// (adjustment, wireframe or environment background layers, Advanced 3D).
|
||||
pub(crate) fn gpu_run(r: &Renderer, ctx: &EvalCtx, run: &[&Layer], out: (u32, u32)) -> Option<Run3d> {
|
||||
if run.is_empty() || run.iter().any(|l| l.switches.adjustment || l.environment_background || l.switches.quality == effectcraft_project::Quality::Wireframe)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
if ctx.comp.renderer == effectcraft_project::Renderer::Advanced3D && r.collapse3d.is_none() {
|
||||
return None;
|
||||
}
|
||||
let g = gather(r, ctx, run, out);
|
||||
let planes: Vec<Plane3d> = g
|
||||
.items
|
||||
.into_iter()
|
||||
.chain(g.extra)
|
||||
.map(|it| Plane3d {
|
||||
layer_id: it.layer.id.0 as u32,
|
||||
mode: it.layer.blend_mode,
|
||||
buf: it.buf,
|
||||
bicubic: it.bicubic,
|
||||
geos: it.geos,
|
||||
mat: it.mat,
|
||||
opacity: it.opacity,
|
||||
draw: it.draw,
|
||||
matte: it.matte,
|
||||
preserve: it.preserve,
|
||||
winv: it.winv,
|
||||
bounds: it.bounds,
|
||||
bbox: it.bbox,
|
||||
order: it.order,
|
||||
})
|
||||
.collect();
|
||||
let casters = planes.iter().enumerate().filter(|(_, p)| p.mat.casts_shadows != 0 && !p.geos.is_empty()).map(|(i, _)| i).collect();
|
||||
Some(Run3d { planes, casters, lights: g.lights, seed: run[0].id.0 as u32 })
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ auxiliary render pass
|
||||
|
||||
/// One surface seen through an output pixel (aux pass).
|
||||
|
|
|
|||
|
|
@ -17,6 +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, Run3d};
|
||||
pub use light::{LightState, Material, lights_at};
|
||||
|
||||
use effectcraft_geom::Mat3;
|
||||
|
|
|
|||
|
|
@ -163,17 +163,31 @@ accelerator is attached and the project's renderer is the GPU). A GPU frame walk
|
|||
sampling (nearest, bilinear, Catmull-Rom bicubic, the same minification pre-filter), motion-blur
|
||||
sub-samples are accumulated, and track mattes, Preserve Transparency, layer style passes,
|
||||
knockout, all 38 blend modes (a WGSL port of `color::blend`), the 8/16 bpc clamp-and-quantise steps
|
||||
and colour-space conversions run on the GPU. 3D runs, adjustment layers and wireframes run on the
|
||||
CPU between GPU steps (read back, draw, upload). GPU effects (`effects::GPU_EFFECTS`: Gaussian,
|
||||
Fast Box and Directional Blur, Glow, Levels, Curves, Hue/Saturation, Tint, Fill, Gradient Ramp,
|
||||
Fractal Noise, Drop Shadow, Brightness & Contrast, Exposure, Invert, Transform) repeat the CPU
|
||||
effect's steps (padding, box radii, parameters) as kernels; consecutive GPU effects run as one
|
||||
and colour-space conversions run on the GPU. Classic 3D runs composite on the GPU too
|
||||
(`gpu::classic3d`): the render crate prepares the planes (`Renderer::prepare_3d_run`: layer buffers
|
||||
with their bokeh depth of field, homographies per motion-blur sub-sample, materials, lights, track
|
||||
mattes), the GPU packs the buffers into one atlas texture and one kernel does the per-pixel
|
||||
fragment sort (intersecting planes, coplanar stack order), Blinn-Phong lighting, ray-cast shadows
|
||||
against the caster planes (Shadow Diffusion, Light Transmission) and blending. Adjustment layers
|
||||
run their effect stacks on the GPU-resident comp (`Renderer::run_effects_on` with an `FxTarget`):
|
||||
runs of GPU effects stay on the device and only non-GPU effects read back and upload. Advanced 3D
|
||||
compositing and wireframes still run on the CPU between GPU steps (read back, draw, upload). GPU
|
||||
effects (`effects::GPU_EFFECTS`, 58 of them: blurs, colour, keying incl. Key Light, distortion,
|
||||
transitions, generators, noise and grain; see [effects.md](effects.md)) repeat the CPU effect's
|
||||
steps (padding, box radii, parameters, hashes) as kernels; consecutive GPU effects run as one
|
||||
chain with one upload and one readback. Tests render scenes on both paths and compare them
|
||||
(≤ 1/255 at 8 bpc, ≤ 1e-3 at 32 bpc); they skip without an adapter. The desktop viewer builds the
|
||||
`Gpu` on egui-wgpu's device and shows frames from GPU textures without reading them back
|
||||
(`ui-egui::frames`); headless renders, the CLI (unless `--gpu`) and CI use the CPU. On the web
|
||||
(WebGPU) the GPU composites viewer frames; steps that need a readback fall back to the CPU (the
|
||||
Info panel's pixel readout reads GPU frames back asynchronously).
|
||||
Info panel's pixel readout reads GPU frames back asynchronously). **GPU particles**
|
||||
(`effects::psim`, `gpu::particles`): CC Particle World, CC Particle Systems II and Particle
|
||||
Playground cannons without interacting forces evolve every particle independently, so the GPU
|
||||
backend (reached through `EffectHost::particles` when the GPU compositor is active) simulates one
|
||||
particle per invocation from the CPU's birth schedule and keeps per-key state checkpoints on the
|
||||
GPU, like `SimCache`; the CPU simulation stays the oracle (tests compare ids and positions).
|
||||
`effectcraft-cli bench --gpu` reports CPU vs GPU ms/frame, speed-up and pixel agreement for every
|
||||
comp plus an adjustment-layer comp.
|
||||
|
||||
**Colour and bit depth** (`crates/render/src/color.rs`, `crates/color/src/space.rs`). Pixels are
|
||||
`f32`, but 8 and 16 bpc projects clamp and quantise each layer after its source and masks and
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
EffectCraft ships 304 effects with 2446 parameters in total, grouped into the same categories as the Effects & Presets panel. Every effect is our own implementation, written from public behaviour descriptions and standard image-processing literature. Parameter names, order, popup options, units, defaults and ranges follow the reference application so that projects, expressions and muscle memory carry over.
|
||||
|
||||
- **GPU**: 25 effects also run on the GPU compositor with identical results.
|
||||
- **GPU**: 59 effects also run on the GPU compositor with identical results.
|
||||
- **32**: 304 effects process 32-bit float (HDR, overbright) pixels without clamping.
|
||||
- **Status**: 262 implemented in full, 42 partial (what is missing is listed).
|
||||
|
||||
|
|
@ -49,16 +49,16 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
|||
| Bilateral Blur | `ec.blur.bilateral` | 3 | | 32 | Implemented |
|
||||
| CC Cross Blur | `ec.blur.cccross` | 3 | | 32 | Implemented |
|
||||
| CC Radial Blur | `ec.blur.ccradial` | 4 | | 32 | Implemented |
|
||||
| CC Radial Fast Blur | `ec.blur.ccradialfast` | 3 | | 32 | Implemented |
|
||||
| CC Radial Fast Blur | `ec.blur.ccradialfast` | 3 | GPU | 32 | Implemented |
|
||||
| CC Vector Blur | `ec.blur.ccvector` | 4 | | 32 | Implemented |
|
||||
| Camera Lens Blur | `ec.blur.cameralens` | 10 | | 32 | Partial: no Diffraction Fringe or Blur Map layer |
|
||||
| Camera Lens Blur | `ec.blur.cameralens` | 10 | GPU | 32 | Partial: no Diffraction Fringe or Blur Map layer |
|
||||
| Camera-Shake Deblur | `ec.blur.camerashakedeblur` | 8 | | 32 | Implemented |
|
||||
| Channel Blur | `ec.blur.channel` | 6 | | 32 | Implemented |
|
||||
| Compound Blur | `ec.blur.compound` | 4 | | 32 | Implemented |
|
||||
| Directional Blur | `ec.blur.directional` | 2 | GPU | 32 | Implemented |
|
||||
| Fast Box Blur | `ec.blur.fastbox` | 4 | GPU | 32 | Implemented |
|
||||
| Gaussian Blur | `ec.blur.gaussian` | 3 | GPU | 32 | Implemented |
|
||||
| Radial Blur | `ec.blur.radial` | 4 | | 32 | Implemented |
|
||||
| Radial Blur | `ec.blur.radial` | 4 | GPU | 32 | Implemented |
|
||||
| Sharpen | `ec.blur.sharpen` | 1 | | 32 | Implemented |
|
||||
| Smart Blur | `ec.blur.smart` | 3 | | 32 | Implemented |
|
||||
| Unsharp Mask | `ec.blur.unsharp` | 3 | | 32 | Implemented |
|
||||
|
|
@ -88,7 +88,7 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
|||
| Auto Color | `ec.color.autocolor` | 6 | | 32 | Implemented |
|
||||
| Auto Contrast | `ec.color.autocontrast` | 5 | | 32 | Implemented |
|
||||
| Auto Levels | `ec.color.autolevels` | 5 | | 32 | Implemented |
|
||||
| Black & White | `ec.color.blackwhite` | 8 | | 32 | Implemented |
|
||||
| Black & White | `ec.color.blackwhite` | 8 | GPU | 32 | Implemented |
|
||||
| Brightness & Contrast | `ec.color.brightnesscontrast` | 3 | GPU | 32 | Implemented |
|
||||
| Broadcast Colors | `ec.color.broadcast` | 3 | | 32 | Implemented |
|
||||
| CC Color Neutralizer | `ec.color.cccolorneutralizer` | 11 | | 32 | Implemented |
|
||||
|
|
@ -97,12 +97,12 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
|||
| CC Toner | `ec.color.cctoner` | 7 | | 32 | Implemented |
|
||||
| Change Color | `ec.color.changecolor` | 9 | | 32 | Implemented |
|
||||
| Change to Color | `ec.color.changetocolor` | 9 | | 32 | Implemented |
|
||||
| Channel Mixer | `ec.color.channelmixer` | 13 | | 32 | Implemented |
|
||||
| Color Balance | `ec.color.colorbalance` | 10 | | 32 | Implemented |
|
||||
| Channel Mixer | `ec.color.channelmixer` | 13 | GPU | 32 | Implemented |
|
||||
| Color Balance | `ec.color.colorbalance` | 10 | GPU | 32 | Implemented |
|
||||
| Color Balance (HLS) | `ec.color.colorbalancehls` | 3 | | 32 | Implemented |
|
||||
| Color Link | `ec.color.colorlink` | 6 | | 32 | Implemented |
|
||||
| Color Stabilizer | `ec.color.colorstabilizer` | 6 | | 32 | Implemented |
|
||||
| Colorama | `ec.color.colorama` | 5 | | 32 | Partial: no Add Phase, editable Output Cycle, Modify, Pixel Selection or Masking |
|
||||
| Colorama | `ec.color.colorama` | 5 | GPU | 32 | Partial: no Add Phase, editable Output Cycle, Modify, Pixel Selection or Masking |
|
||||
| Curves | `ec.color.curves` | 0 | GPU | 32 | Partial: no Channel popup |
|
||||
| Equalize | `ec.color.equalize` | 2 | | 32 | Implemented |
|
||||
| Exposure | `ec.color.exposure` | 14 | GPU | 32 | Implemented |
|
||||
|
|
@ -111,7 +111,7 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
|||
| Leave Color | `ec.color.leavecolor` | 5 | | 32 | Implemented |
|
||||
| Levels | `ec.color.levels` | 7 | GPU | 32 | Partial: master channel only (no Channel popup) |
|
||||
| Levels (Individual Controls) | `ec.color.levelsic` | 27 | | 32 | Implemented |
|
||||
| Lumetri Color | `ec.color.lumetri` | 38 | | 32 | Partial: no HSL Secondary, hue/saturation curves, HDR mode or look files |
|
||||
| Lumetri Color | `ec.color.lumetri` | 38 | GPU | 32 | Partial: no HSL Secondary, hue/saturation curves, HDR mode or look files |
|
||||
| OCIO CDL Transform | `ec.color.ociocdl` | 12 | | 32 | Implemented |
|
||||
| OCIO Color Space Transform | `ec.color.ociocolorspace` | 7 | | 32 | Implemented |
|
||||
| OCIO Display Transform | `ec.color.ociodisplay` | 9 | | 32 | Implemented |
|
||||
|
|
@ -119,11 +119,11 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
|||
| OCIO Look Transform | `ec.color.ociolook` | 6 | | 32 | Implemented |
|
||||
| PS Arbitrary Map | `ec.color.psarbitrarymap` | 2 | | 32 | Implemented |
|
||||
| Photo Filter | `ec.color.photofilter` | 4 | | 32 | Implemented |
|
||||
| Selective Color | `ec.color.selectivecolor` | 37 | | 32 | Partial: simplified Colors / Details layout |
|
||||
| Selective Color | `ec.color.selectivecolor` | 37 | GPU | 32 | Partial: simplified Colors / Details layout |
|
||||
| Shadow/Highlight | `ec.color.shadowhighlight` | 14 | | 32 | Implemented |
|
||||
| Tint | `ec.color.tint` | 3 | GPU | 32 | Implemented |
|
||||
| Tritone | `ec.color.tritone` | 4 | | 32 | Implemented |
|
||||
| Vibrance | `ec.color.vibrance` | 2 | | 32 | Implemented |
|
||||
| Tritone | `ec.color.tritone` | 4 | GPU | 32 | Implemented |
|
||||
| Vibrance | `ec.color.vibrance` | 2 | GPU | 32 | Implemented |
|
||||
| Video Limiter | `ec.color.videolimiter` | 5 | | 32 | Implemented |
|
||||
|
||||
## Distort
|
||||
|
|
@ -131,13 +131,13 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
|||
| Effect | Id | Params | GPU | 32 | Status |
|
||||
|---|---|---:|:-:|:-:|---|
|
||||
| Bezier Warp | `ec.distort.bezierwarp` | 13 | | 32 | Implemented |
|
||||
| Bulge | `ec.distort.bulge` | 6 | | 32 | Implemented |
|
||||
| Bulge | `ec.distort.bulge` | 6 | GPU | 32 | Implemented |
|
||||
| CC Bend It | `ec.distort.ccbendit` | 4 | | 32 | Implemented |
|
||||
| CC Bender | `ec.distort.ccbender` | 5 | | 32 | Implemented |
|
||||
| CC Blobbylize | `ec.distort.ccblobbylize` | 15 | | 32 | Implemented |
|
||||
| CC Flo Motion | `ec.distort.ccflomotion` | 7 | | 32 | Implemented |
|
||||
| CC Griddler | `ec.distort.ccgriddler` | 5 | | 32 | Implemented |
|
||||
| CC Lens | `ec.distort.cclens` | 3 | | 32 | Implemented |
|
||||
| CC Lens | `ec.distort.cclens` | 3 | GPU | 32 | Implemented |
|
||||
| CC Page Turn | `ec.distort.ccpageturn` | 7 | | 32 | Implemented |
|
||||
| CC Power Pin | `ec.distort.ccpowerpin` | 10 | | 32 | Implemented |
|
||||
| CC Ripple Pulse | `ec.distort.ccripplepulse` | 5 | | 32 | Implemented |
|
||||
|
|
@ -148,27 +148,27 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
|||
| CC Tiler | `ec.distort.cctiler` | 3 | | 32 | Implemented |
|
||||
| Corner Pin | `ec.distort.cornerpin` | 4 | | 32 | Implemented |
|
||||
| Detail-preserving Upscale | `ec.distort.upscale` | 4 | | 32 | Implemented |
|
||||
| Displacement Map | `ec.distort.displacementmap` | 8 | | 32 | Implemented |
|
||||
| Displacement Map | `ec.distort.displacementmap` | 8 | GPU | 32 | Implemented |
|
||||
| Liquify | `ec.distort.liquify` | 12 | | 32 | Implemented |
|
||||
| Magnify | `ec.distort.magnify` | 10 | | 32 | Implemented |
|
||||
| Mesh Warp | `ec.distort.meshwarp` | 3 | | 32 | Implemented |
|
||||
| Mesh Warp | `ec.distort.meshwarp` | 3 | GPU | 32 | Implemented |
|
||||
| Mirror | `ec.distort.mirror` | 2 | | 32 | Implemented |
|
||||
| Offset | `ec.distort.offset` | 2 | | 32 | Implemented |
|
||||
| Optics Compensation | `ec.distort.opticscompensation` | 5 | | 32 | Partial: no Optimal Pixels |
|
||||
| Polar Coordinates | `ec.distort.polar` | 2 | | 32 | Implemented |
|
||||
| Puppet | `ec.distort.puppet` | 1 | | 32 | Implemented |
|
||||
| Reshape | `ec.distort.reshape` | 6 | | 32 | Partial: no correspondence points |
|
||||
| Ripple | `ec.distort.ripple` | 7 | | 32 | Implemented |
|
||||
| Ripple | `ec.distort.ripple` | 7 | GPU | 32 | Implemented |
|
||||
| Rolling Shutter Repair | `ec.distort.rollingshutterrepair` | 5 | | 32 | Partial: Pixel Motion Detail has no effect |
|
||||
| Smear | `ec.distort.smear` | 8 | | 32 | Implemented |
|
||||
| Spherize | `ec.distort.spherize` | 2 | | 32 | Implemented |
|
||||
| Transform | `ec.distort.transform` | 10 | GPU | 32 | Partial: no shutter-angle motion blur |
|
||||
| Turbulent Displace | `ec.distort.turbulentdisplace` | 11 | | 32 | Partial: no locked pinning variants |
|
||||
| Twirl | `ec.distort.twirl` | 3 | | 32 | Implemented |
|
||||
| Turbulent Displace | `ec.distort.turbulentdisplace` | 11 | GPU | 32 | Partial: no locked pinning variants |
|
||||
| Twirl | `ec.distort.twirl` | 3 | GPU | 32 | Implemented |
|
||||
| Twirl (Legacy) | `ec.distort.twirllegacy` | 3 | | 32 | Implemented |
|
||||
| Warp | `ec.distort.warp` | 5 | | 32 | Implemented |
|
||||
| Warp Stabilizer | `ec.distort.warpstabilizer` | 20 | | 32 | Implemented |
|
||||
| Wave Warp | `ec.distort.wavewarp` | 7 | | 32 | Implemented |
|
||||
| Wave Warp | `ec.distort.wavewarp` | 7 | GPU | 32 | Implemented |
|
||||
|
||||
## Expression Controls
|
||||
|
||||
|
|
@ -188,7 +188,7 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
|||
|
||||
| Effect | Id | Params | GPU | 32 | Status |
|
||||
|---|---|---:|:-:|:-:|---|
|
||||
| 4-Color Gradient | `ec.generate.fourcolor` | 12 | | 32 | Implemented |
|
||||
| 4-Color Gradient | `ec.generate.fourcolor` | 12 | GPU | 32 | Implemented |
|
||||
| Advanced Lightning | `ec.generate.advancedlightning` | 16 | | 32 | Partial: no Alpha Obstacle; most Expert Settings missing |
|
||||
| Audio Spectrum | `ec.generate.audiospectrum` | 23 | | 32 | Implemented |
|
||||
| Audio Waveform | `ec.generate.audiowaveform` | 17 | | 32 | Implemented |
|
||||
|
|
@ -198,15 +198,15 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
|||
| CC Light Rays | `ec.generate.cclightrays` | 8 | | 32 | Implemented |
|
||||
| CC Light Sweep | `ec.generate.cclightsweep` | 9 | | 32 | Implemented |
|
||||
| CC Threads | `ec.generate.ccthreads` | 8 | | 32 | Implemented |
|
||||
| Cell Pattern | `ec.generate.cellpattern` | 14 | | 32 | Partial: HQ variants render like the standard ones |
|
||||
| Checkerboard | `ec.generate.checkerboard` | 10 | | 32 | Implemented |
|
||||
| Cell Pattern | `ec.generate.cellpattern` | 14 | GPU | 32 | Partial: HQ variants render like the standard ones |
|
||||
| Checkerboard | `ec.generate.checkerboard` | 10 | GPU | 32 | Implemented |
|
||||
| Circle | `ec.generate.circle` | 11 | | 32 | Implemented |
|
||||
| Ellipse | `ec.generate.ellipse` | 8 | | 32 | Implemented |
|
||||
| Eyedropper Fill | `ec.generate.eyedropperfill` | 5 | | 32 | Implemented |
|
||||
| Fill | `ec.generate.fill` | 3 | GPU | 32 | Partial: no Fill Mask / All Masks / feather |
|
||||
| Fractal | `ec.generate.fractal` | 21 | | 32 | Implemented |
|
||||
| Gradient Ramp | `ec.generate.gradientramp` | 7 | GPU | 32 | Implemented |
|
||||
| Grid | `ec.generate.grid` | 12 | | 32 | Implemented |
|
||||
| Grid | `ec.generate.grid` | 12 | GPU | 32 | Implemented |
|
||||
| Lens Flare | `ec.generate.lensflare` | 4 | | 32 | Implemented |
|
||||
| Paint Bucket | `ec.generate.paintbucket` | 11 | | 32 | Implemented |
|
||||
| Radio Waves | `ec.generate.radiowaves` | 17 | | 32 | Partial: no Image Contours or Mask wave types, Parameters Are Set At, reflection or stroke profile |
|
||||
|
|
@ -244,8 +244,8 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
|||
| Extract | `ec.key.extract` | 6 | | 32 | Implemented |
|
||||
| Inner/Outer Key | `ec.key.innerouter` | 10 | | 32 | Partial: one additional mask per side; no cleanup strokes |
|
||||
| Key Cleaner | `ec.key.keycleaner` | 4 | | 32 | Implemented |
|
||||
| Key Light | `ec.keying.keylight` | 42 | | 32 | Partial: no Source Crops X/Y Method or Edge Colour; no colour suppression / balancing |
|
||||
| Linear Color Key | `ec.key.linearcolor` | 6 | | 32 | Implemented |
|
||||
| Key Light | `ec.keying.keylight` | 42 | GPU | 32 | Partial: no Source Crops X/Y Method or Edge Colour; no colour suppression / balancing |
|
||||
| Linear Color Key | `ec.key.linearcolor` | 6 | GPU | 32 | Implemented |
|
||||
| Screen Key | `ec.key.screen` | 11 | | 32 | Implemented |
|
||||
| Unmult | `ec.key.unmult` | 6 | | 32 | Implemented |
|
||||
|
||||
|
|
@ -264,14 +264,14 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
|||
|
||||
| Effect | Id | Params | GPU | 32 | Status |
|
||||
|---|---|---:|:-:|:-:|---|
|
||||
| Add Grain | `ec.noise.addgrain` | 22 | | 32 | Partial: no preview region, presets, channel balance or temporal controls |
|
||||
| Add Grain | `ec.noise.addgrain` | 22 | GPU | 32 | Partial: no preview region, presets, channel balance or temporal controls |
|
||||
| Curl Noise | `ec.noise.curlnoise` | 10 | | 32 | Implemented |
|
||||
| Dust & Scratches | `ec.noise.dustscratches` | 3 | | 32 | Implemented |
|
||||
| Fractal Noise | `ec.noise.fractal` | 26 | GPU | 32 | Implemented |
|
||||
| Match Grain | `ec.noise.matchgrain` | 25 | | 32 | Partial: no preview region, presets, sampling or temporal controls |
|
||||
| Median | `ec.noise.median` | 2 | | 32 | Implemented |
|
||||
| Median (Legacy) | `ec.noise.medianlegacy` | 2 | | 32 | Implemented |
|
||||
| Noise | `ec.noise.noise` | 3 | | 32 | Implemented |
|
||||
| Noise | `ec.noise.noise` | 3 | GPU | 32 | Implemented |
|
||||
| Noise Alpha | `ec.noise.noisealpha` | 8 | | 32 | Implemented |
|
||||
| Noise HLS | `ec.noise.noisehls` | 6 | | 32 | Implemented |
|
||||
| Noise HLS Auto | `ec.noise.noisehlsauto` | 6 | | 32 | Implemented |
|
||||
|
|
@ -354,10 +354,10 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
|||
| CC Vignette | `ec.stylize.ccvignette` | 4 | | 32 | Implemented |
|
||||
| Cartoon | `ec.stylize.cartoon` | 11 | | 32 | Partial: no Edge Enhancement |
|
||||
| Color Emboss | `ec.stylize.coloremboss` | 4 | | 32 | Implemented |
|
||||
| Emboss | `ec.stylize.emboss` | 4 | | 32 | Implemented |
|
||||
| Find Edges | `ec.stylize.findedges` | 2 | | 32 | Implemented |
|
||||
| Emboss | `ec.stylize.emboss` | 4 | GPU | 32 | Implemented |
|
||||
| Find Edges | `ec.stylize.findedges` | 2 | GPU | 32 | Implemented |
|
||||
| Glow | `ec.stylize.glow` | 13 | GPU | 32 | Partial: no Glow Operation or arbitrary colour map |
|
||||
| Mosaic | `ec.stylize.mosaic` | 3 | | 32 | Implemented |
|
||||
| Mosaic | `ec.stylize.mosaic` | 3 | GPU | 32 | Implemented |
|
||||
| Motion Tile | `ec.stylize.motiontile` | 8 | | 32 | Implemented |
|
||||
| Posterize | `ec.stylize.posterize` | 1 | | 32 | Implemented |
|
||||
| Roughen Edges | `ec.stylize.roughenedges` | 13 | | 32 | Implemented |
|
||||
|
|
@ -402,11 +402,11 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
|||
| CC Twister | `ec.transition.cctwister` | 5 | | 32 | Implemented |
|
||||
| CC WarpoMatic | `ec.transition.ccwarpomatic` | 8 | | 32 | Implemented |
|
||||
| Card Wipe | `ec.transition.cardwipe` | 12 | | 32 | Partial: 2D flip only; no back layer picker, camera, lighting or jitter |
|
||||
| Gradient Wipe | `ec.transition.gradientwipe` | 5 | | 32 | Implemented |
|
||||
| Gradient Wipe | `ec.transition.gradientwipe` | 5 | GPU | 32 | Implemented |
|
||||
| Iris Wipe | `ec.transition.iriswipe` | 7 | | 32 | Implemented |
|
||||
| Linear Wipe | `ec.transition.linearwipe` | 3 | | 32 | Implemented |
|
||||
| Radial Wipe | `ec.transition.radialwipe` | 5 | | 32 | Implemented |
|
||||
| Venetian Blinds | `ec.transition.venetian` | 4 | | 32 | Implemented |
|
||||
| Linear Wipe | `ec.transition.linearwipe` | 3 | GPU | 32 | Implemented |
|
||||
| Radial Wipe | `ec.transition.radialwipe` | 5 | GPU | 32 | Implemented |
|
||||
| Venetian Blinds | `ec.transition.venetian` | 4 | GPU | 32 | Implemented |
|
||||
|
||||
## Utility
|
||||
|
||||
|
|
|
|||
|
|
@ -20,12 +20,11 @@ Animation 94%, Masks 94%, Preview 94%, Interface 93%, Shapes 93%, 3D 91%, Audio
|
|||
Tracking 88%, Effects 85%, Web 65%.
|
||||
|
||||
What is left, in priority order: the web app's depth (threads, storage, audio, non-blocking
|
||||
renders); stroke taper/wave and multi-segment dashes; variable mask feather points; camera iris/bokeh and focus-link commands; GPU
|
||||
rendering of 3D runs and adjustment layers; Render Queue field render, crop/resize and templates;
|
||||
renders); stroke taper/wave and multi-segment dashes; variable mask feather points; camera iris/bokeh and focus-link commands; Render Queue field render, crop/resize and templates;
|
||||
approximated effects (Subspace Warp, Key Cleaner) and Liquify's viewer brush; OpenType features and variable
|
||||
font axes; AI/EPS/PDF import; more codecs; 59 preferences not yet wired; ScriptUI;
|
||||
Lumetri Scopes, Footage, Media Browser and Metadata panels; Content-Aware Fill; face tracking; and
|
||||
the "better than After Effects" items (a plug-in API, branching history, GPU particles).
|
||||
the "better than After Effects" items (a plug-in API, branching history; GPU particles landed in M12.7).
|
||||
|
||||
The sections below are the original audit (morning of 2 October, ≈ 64%) and its updates, kept for
|
||||
history.
|
||||
|
|
@ -120,11 +119,11 @@ the disk cache in the formats wave; the 3D Camera Tracker in M12.6.)
|
|||
| Text | ≈ 91% | 0.8 | no OpenType feature panel, no extruded strokes, variable-axis animation changes outlines but not advances (vertical Roman / Tate-Chu-Yoko, forced LTR paragraphs, caret on animated and path text, Variable Font Axes and Lottie style runs landed in M13.5; extruded, bevelled text in M7.6; per-character styles, paragraph settings, on-canvas editing and the `sourceText` style API in M9.9–M9.10) |
|
||||
| Web | 85% | 1.0 | viewer frames and Roto Brush propagation still on the page's thread; GPU effects in the browser (browser storage, Web Audio, Web Worker renders/analyses, WebGPU viewer and offline install landed in M15.2) |
|
||||
| 3D | 85% | 6.5 | multi-view layouts, iris shapes in Advanced 3D's depth of field, collapsed 3D precomps in Advanced 3D, the Extended Viewer; stereo rigs, orbit nulls, lights controlled by the camera, cameras/lights from glTF models, environment backgrounds, Advanced 3D motion blur, blend modes and track mattes landed in M7.7; Classic 3D iris-shaped bokeh with highlights, progressive depth of field on tilted layers and the focus-link commands landed in M13.5; Advanced 3D (glTF/OBJ models, primitives, extruded text and shapes, PBR, image-based light, shadow maps, GPU rasteriser) in M7.4–M7.6 |
|
||||
| Effects | ≈ 80% | 4.5 | GPU versions of more effects and the missing controls listed as partial in [effects.md](effects.md) (every After Effects effect exists since M9.11, M12.5 and M12.6; parameter names, order, twirl-downs, popups, units and defaults were aligned in M9.12) |
|
||||
| Effects | ≈ 80% | 4.0 | GPU versions of the remaining effects (58 run on the GPU since M12.7) and the missing controls listed as partial in [effects.md](effects.md) (every After Effects effect exists since M9.11, M12.5 and M12.6; parameter names, order, twirl-downs, popups, units and defaults were aligned in M9.12) |
|
||||
| Interface | 74% | 5.0 | a richer Learn area (Timeline outline and Project panel columns scroll horizontally, the Layer Style dialog, ROI resize handles, Pan Behind snapping and 3D Reference Axes landed in M13.5; native macOS menu bar, Timeline columns/search/reveal-add, Home screen with recent projects and all AE workspaces landed; viewer rulers/snapping/channels/snapshots landed in M0.13) |
|
||||
| Project | ≈ 68% | 5.5 | auto-save, folder moves, OCIO displays beyond the built-in tone map (Color Engine with OCIO/ACES working spaces, HDR compand/tone mapping, Rec. 2100 PQ/HLG output, Feet + Frames, display colour management, Simulate Output and the locked viewer landed in M7.7; proxies and Interpret Footage fields / pixel aspect / alpha guess landed: PRJ-8, PRJ-3) |
|
||||
| Masks & roto | 74% | 5.0 | Roto Brush's learned (3.0) segmentation model (variable-width mask feather points with the Mask Feather tool landed in M13.5; mask tracking and Mask Interpolation landed in M6.6; Roto Brush & Refine Edge with graph-cut segmentation, flow propagation, edge matting, decontamination and Freeze in M6.7) |
|
||||
| Preview | 62% | 5.0 | GPU 3D and adjustment layers (persistent disk cache with the blue cache bar landed; region of interest, snapshots, exposure and Fast Previews landed in M0.13) |
|
||||
| Preview | 66% | 4.0 | GPU bokeh depth of field, wireframes and Advanced 3D compositing (Classic 3D runs and adjustment layers composite on the GPU since M12.7; persistent disk cache with the blue cache bar landed; region of interest, snapshots, exposure and Fast Previews landed in M0.13) |
|
||||
| Tracking | ≈ 85% | 2.5 | face tracking, Subspace Warp's mesh warp, lens distortion in the camera solve (Rolling Shutter Repair landed in M9.11; point tracker, mask tracking, Warp Stabilizer and the 3D Camera Tracker landed in M6.x / M12.5 / M12.6) |
|
||||
| Paint | 0% | 4.0 | Brush, Clone Stamp, Eraser |
|
||||
|
||||
|
|
@ -171,8 +170,8 @@ taper/wave, Advanced 3D's DOF has no iris shapes, and variable axes don't change
|
|||
1. ~~On-canvas text editing and per-character styles~~ (landed: M9.9–M9.10).
|
||||
2. Effect Controls widgets: angle dial, point crosshair, eyedropper, curves and levels editors.
|
||||
3. ~~Viewer basics: snapping, rulers, channel view, snapshots, exposure, a drawable region of interest.~~ (M0.13)
|
||||
4. ~~A GPU (wgpu) compositor, then GPU effects~~ (M12.2: 2D compositing and 16 GPU effects; 3D runs
|
||||
and adjustment layers still composite on the CPU).
|
||||
4. ~~A GPU (wgpu) compositor, then GPU effects~~ (M12.2: 2D compositing and 16 GPU effects; M12.7:
|
||||
Classic 3D runs, adjustment layers, 58 GPU effects and GPU particles).
|
||||
5. ~~Pen tool for shape paths, shape vertex editing, free transform~~ (M6.5); the Layer viewer.
|
||||
6. ~~Motion-path handles in the viewer; graph editor transform box and snapping.~~ (M5.8)
|
||||
7. Point tracking and stabilization (in progress).
|
||||
|
|
|
|||
Loading…
Reference in New Issue