M13.29: GPU effects part D (particles and remaining fallbacks)
GPU render passes for CC Particle World / Systems II, Particle Playground, CC Ball Action, CC Pixel Polly, CC Scatterize and Curl Noise (fx_particles, chunked multi-pass raster); Shatter wireframe views, Foam extras, Timewarp with a Matte Layer, strong Warp Fisheye/Twist (CPU f64 inverse map) and custom .ocio configs no longer fall back to the CPU. CPU refactors pinned by particle_golden / timewarp_golden / sim_golden hashes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
b9f09869eb
commit
1c855c3e07
|
|
@ -91,36 +91,12 @@ const CPU_ONLY_CONTROLS: &[(&str, &[&str])] = &[
|
|||
];
|
||||
|
||||
/// Whether the GPU kernel of effect `id` covers the instance's current settings (see
|
||||
/// [`CPU_ONLY_CONTROLS`]; plus Cell Pattern's HQ variants, Turbulent Displace's locked
|
||||
/// pinning, Warp's strong Fisheye / Twist and Timewarp with a Matte Layer).
|
||||
/// [`CPU_ONLY_CONTROLS`]; plus Cell Pattern's HQ variants and Turbulent Displace's locked
|
||||
/// pinning).
|
||||
pub fn gpu_supported(id: &str, ctx: &crate::EffectCtx) -> bool {
|
||||
match id {
|
||||
"ec.generate.cellpattern" if (6..=10).contains(&ctx.params.e("cellPattern")) => return false,
|
||||
"ec.distort.turbulentdisplace" if ctx.params.e("pinning") >= 8 => return false,
|
||||
// Shatter's wireframe views; Foam's User Defined texture, Environment Map and flow-map
|
||||
// preview (the GPU draws the rendered pieces and the sprite bubbles only).
|
||||
"ec.sim.shatter" if ctx.params.e("view") != 0 => return false,
|
||||
"ec.sim.foam" => {
|
||||
let set = |id: &str| matches!(ctx.params.get(id), Some(effectcraft_keyframe::Value::Layer(Some(_))));
|
||||
let view = ctx.params.e("view");
|
||||
if (view == 2 && ctx.params.e("rendering/bubbleTexture") == 5)
|
||||
|| (view == 2 && ctx.params.f("rendering/reflectionStrength") > 0.0 && set("rendering/environmentMap"))
|
||||
|| (view == 1 && set("flowMap/flowMap"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Timewarp's Matte Layer (foreground and background on their own vectors).
|
||||
"ec.time.timewarp" if ctx.params.get("matteLayer").and_then(effectcraft_keyframe::Value::as_layer).is_some() => return false,
|
||||
// Fisheye / Twist (a crease at the unit circle) bent past 50 % or with Horizontal /
|
||||
// Vertical Distortion: Newton's inverse wanders chaotically before converging near the
|
||||
// fold, so f32 lands on other pixels than the CPU's f64.
|
||||
"ec.distort.warp"
|
||||
if matches!(ctx.params.e("warpStyle"), 11 | 14)
|
||||
&& (ctx.params.f("bend").abs() > 50.0 || ctx.params.f("horizontalDistortion") != 0.0 || ctx.params.f("verticalDistortion") != 0.0) =>
|
||||
{
|
||||
return false;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
let Some((_, controls)) = CPU_ONLY_CONTROLS.iter().find(|(e, _)| *e == id) else { return true };
|
||||
|
|
|
|||
|
|
@ -663,13 +663,24 @@ fn warp(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
if k == 0.0 && hd == 0.0 && vd == 0.0 {
|
||||
return b;
|
||||
}
|
||||
let style = ctx.params.e("warpStyle");
|
||||
let vertical = ctx.params.e("warpAxis") == 1;
|
||||
let (hx, hy) = (ctx.layer_size[0] * b.scale * 0.5, ctx.layer_size[1] * b.scale * 0.5);
|
||||
if !ctx.adjustment {
|
||||
let pad = ((k.abs() + hd.abs() + vd.abs()) * 0.6 * hx.max(hy)).ceil().min(4096.0);
|
||||
b.pad(pad as u32 + 1);
|
||||
}
|
||||
b.img = remap(&b.img, false, warp_inverse(ctx, &b));
|
||||
b
|
||||
}
|
||||
|
||||
/// Warp's inverse map for buffer geometry `b` (padded): the source point of an output point
|
||||
/// (buffer px), by Newton iterations on the forward warp; `None` = transparent.
|
||||
fn warp_inverse(ctx: &EffectCtx, b: &Buf) -> impl Fn(f64, f64) -> Option<(f64, f64)> + Sync + use<> {
|
||||
let k = ctx.params.f("bend") / 100.0;
|
||||
let hd = ctx.params.f("horizontalDistortion") / 100.0;
|
||||
let vd = ctx.params.f("verticalDistortion") / 100.0;
|
||||
let style = ctx.params.e("warpStyle");
|
||||
let vertical = ctx.params.e("warpAxis") == 1;
|
||||
let (hx, hy) = (ctx.layer_size[0] * b.scale * 0.5, ctx.layer_size[1] * b.scale * 0.5);
|
||||
let c = b.to_px([ctx.layer_size[0] * 0.5, ctx.layer_size[1] * 0.5]);
|
||||
let (hx, hy) = (hx.max(1e-6), hy.max(1e-6));
|
||||
let fwd = move |x: f64, y: f64| -> (f64, f64) {
|
||||
|
|
@ -682,7 +693,7 @@ fn warp(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
x1 *= 1.0 + 0.5 * vd * y1;
|
||||
(c.0 + x1 * hx, c.1 + y1 * hy)
|
||||
};
|
||||
b.img = remap(&b.img, false, |x, y| {
|
||||
move |x, y| {
|
||||
let (mut sx, mut sy) = (x, y);
|
||||
for _ in 0..12 {
|
||||
let (fx, fy) = fwd(sx, sy);
|
||||
|
|
@ -706,8 +717,19 @@ fn warp(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
return None;
|
||||
}
|
||||
Some((sx, sy))
|
||||
});
|
||||
b
|
||||
}
|
||||
}
|
||||
|
||||
/// Warp's inverse map over a `w` × `h` buffer with geometry `b` (its pixels are not read), as
|
||||
/// an image of (source x, source y, 1, 0) per pixel centre, transparent where the warp leaves
|
||||
/// the pixel empty: the plan of the GPU compositor's Warp where the Newton iterations must run
|
||||
/// in f64 to land on the CPU's pixels (strong Fisheye / Twist bends).
|
||||
pub fn warp_inverse_map(ctx: &EffectCtx, b: &Buf, w: u32, h: u32) -> Image {
|
||||
let f = warp_inverse(ctx, b);
|
||||
crate::util::gen_image(w, h, |x, y| match f(x as f64 + 0.5, y as f64 + 0.5) {
|
||||
Some((sx, sy)) => [sx as f32, sy as f32, 1.0, 0.0],
|
||||
None => [0.0; 4],
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Detail-preserving Upscale
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ pub use color2::{Curve, auto_correct_settings, equalize_tables, luma_clip_points
|
|||
pub use color3::OffsetCurve;
|
||||
pub use distort::transform_shutter;
|
||||
pub use distort2::{MAGNIFY_MODES, bezier_grid, parse_mesh};
|
||||
pub use distort3::{RESHAPE_POINTS, ReshapeSetup, SmearSetup, reshape_setup, smear_setup};
|
||||
pub use distort3::{RESHAPE_POINTS, ReshapeSetup, SmearSetup, reshape_setup, smear_setup, warp_inverse_map};
|
||||
pub use distort4::liquify_mesh;
|
||||
use effectcraft_keyframe::Value;
|
||||
use effectcraft_project::build::Ids;
|
||||
|
|
@ -83,7 +83,7 @@ use effectcraft_project::{GroupKind, ParamUi, PropGroup, Property};
|
|||
pub use effectcraft_raster::{AuxChannels, Image};
|
||||
pub use misc::{INVERT_ALPHA, INVERT_CHANNELS, glow_ab_t, glow_operation};
|
||||
pub use sim::particle_state;
|
||||
pub use sim3::playground_state;
|
||||
pub use sim3::{PgBlit, PgPlan, playground_plan, playground_state};
|
||||
pub use stylize2::strobe_on;
|
||||
// CPU helpers the GPU kernels share (effectcraft-gpu).
|
||||
pub use blur2::{camera_lens_plain, camera_lens_spans};
|
||||
|
|
@ -102,7 +102,10 @@ pub use ocio::color_stabilizer_maps;
|
|||
pub use channel3d::{CRYPTO_LAYERS, id_color, selection_hashes};
|
||||
pub use ocio::{ColorOp, Straight, Tf, color_program};
|
||||
pub use sim::{Acc, Blob, Bubble, Post, Shape, Sprite, SpritePlan, Tint, bubble_list, drizzle_drops, mercury_blobs, sprite_plan};
|
||||
pub use sim2::{CausticsSetup, Piece, PiecePlan, PieceTex, WavePlan, Waves, caustics_setup, piece_plan, wave_world_plan};
|
||||
pub use sim2::{
|
||||
CausticsSetup, FoamDisc, FoamEnv, FoamPlan, Piece, PiecePlan, PieceTex, PixelPlan, WavePlan, Waves, caustics_setup, foam_full_plan, piece_plan, pixel_plan,
|
||||
wave_world_plan,
|
||||
};
|
||||
pub use utility::{Lut, load_lut};
|
||||
pub use vr::{equi_dir as vr_equi_dir, rotation as vr_rotation};
|
||||
|
||||
|
|
@ -983,6 +986,14 @@ pub const GPU_EFFECTS: &[&str] = &[
|
|||
"ec.sim.shatter",
|
||||
"ec.sim.carddance",
|
||||
"ec.transition.cardwipe",
|
||||
// effectcraft-gpu fx_particles (particle render passes)
|
||||
"ec.sim.ccparticleworld",
|
||||
"ec.sim.ccparticlesystems2",
|
||||
"ec.sim.particleplayground",
|
||||
"ec.sim.ccballaction",
|
||||
"ec.sim.ccpixelpolly",
|
||||
"ec.sim.ccscatterize",
|
||||
"ec.noise.curlnoise",
|
||||
// effectcraft-gpu fx_light (the CC light family)
|
||||
"ec.generate.cclightrays",
|
||||
"ec.generate.cclightburst",
|
||||
|
|
|
|||
|
|
@ -888,6 +888,88 @@ pub enum ColorOp {
|
|||
/// A LUT (0 nearest, 1 trilinear, 2 tetrahedral) with an optional per-channel shaper, forward
|
||||
/// or inverse (as [`FileXform::apply`]).
|
||||
Lut { lut: Arc<Lut>, shaper: Option<Arc<[Vec<(f32, f32)>; 3]>>, interp: u32, inverse: bool },
|
||||
/// `m × (c + pre) + post` (a custom config's MatrixTransform and RangeTransform).
|
||||
Affine { m: [[f32; 3]; 3], pre: [f32; 3], post: [f32; 3] },
|
||||
/// `max(c, 0) ^ p` per channel (ExponentTransform).
|
||||
Pow([f32; 3]),
|
||||
/// LogTransform / LogAffineTransform, forward or inverse (as `ocio_config::Xf::apply`).
|
||||
LogAffine { base: f32, log_slope: [f32; 3], log_offset: [f32; 3], lin_slope: [f32; 3], lin_offset: [f32; 3], inverse: bool },
|
||||
}
|
||||
|
||||
/// The ops of a LUT / CDL file transform (as [`FileXform::apply`]).
|
||||
fn file_ops(x: &FileXform, interp: u32, inverse: bool, ccc_id: &str, ops: &mut Vec<ColorOp>) {
|
||||
match x {
|
||||
FileXform::Cdl(list) => {
|
||||
let cdl = list.iter().find(|(i, _)| !ccc_id.is_empty() && i == ccc_id).or(list.first()).map(|(_, c)| *c).unwrap_or_default();
|
||||
ops.push(ColorOp::Cdl { cdl, clamp: true, inverse });
|
||||
}
|
||||
FileXform::Lut { lut, shaper } => {
|
||||
ops.push(ColorOp::Lut { lut: Arc::new(lut.clone()), shaper: shaper.clone().map(Arc::new), interp, inverse });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The ops of a custom config's transform `x` (as `Xf::apply`).
|
||||
fn config_xf_ops(x: &crate::ocio_config::Xf, inverse: bool, ops: &mut Vec<ColorOp>) {
|
||||
use crate::ocio_config::Xf;
|
||||
let f3 = |v: &[f64; 3]| v.map(|x| x as f32);
|
||||
match x {
|
||||
Xf::Inverse(x) => config_xf_ops(x, !inverse, ops),
|
||||
Xf::Matrix { m, offset } => {
|
||||
if !inverse {
|
||||
let m = [0, 1, 2].map(|r| [m[r * 4] as f32, m[r * 4 + 1] as f32, m[r * 4 + 2] as f32]);
|
||||
ops.push(ColorOp::Affine { m, pre: [0.0; 3], post: [offset[0] as f32, offset[1] as f32, offset[2] as f32] });
|
||||
} else if let Some(inv) = crate::ocio_config::mat3_inverse(m) {
|
||||
let m = inv.map(|r| r.map(|v| v as f32));
|
||||
ops.push(ColorOp::Affine { m, pre: [-offset[0] as f32, -offset[1] as f32, -offset[2] as f32], post: [0.0; 3] });
|
||||
}
|
||||
}
|
||||
Xf::File { xf: Some(x), interp, ccc, .. } => file_ops(x, *interp, inverse, ccc, ops),
|
||||
Xf::File { xf: None, .. } | Xf::Unsupported(_) => {}
|
||||
Xf::Exponent(e) => ops.push(ColorOp::Pow([0, 1, 2].map(|k| (if inverse { 1.0 / e[k].max(1e-9) } else { e[k] }) as f32))),
|
||||
Xf::Log { base } => {
|
||||
ops.push(ColorOp::LogAffine { base: *base as f32, log_slope: [1.0; 3], log_offset: [0.0; 3], lin_slope: [1.0; 3], lin_offset: [0.0; 3], inverse })
|
||||
}
|
||||
Xf::LogAffine { base, log_slope, log_offset, lin_slope, lin_offset } => ops.push(ColorOp::LogAffine {
|
||||
base: *base as f32,
|
||||
log_slope: f3(log_slope),
|
||||
log_offset: f3(log_offset),
|
||||
lin_slope: f3(lin_slope),
|
||||
lin_offset: f3(lin_offset),
|
||||
inverse,
|
||||
}),
|
||||
Xf::Cdl(cdl) => ops.push(ColorOp::Cdl { cdl: *cdl, clamp: true, inverse }),
|
||||
Xf::Range { min_in, max_in, min_out, max_out } => {
|
||||
let (a0, a1, b0, b1) = if inverse { (*min_out, *max_out, *min_in, *max_in) } else { (*min_in, *max_in, *min_out, *max_out) };
|
||||
let k = if (a1 - a0).abs() > 1e-12 { (b1 - b0) / (a1 - a0) } else { 0.0 };
|
||||
let k = k as f32;
|
||||
ops.push(ColorOp::Affine { m: [[k, 0.0, 0.0], [0.0, k, 0.0], [0.0, 0.0, k]], pre: [-a0 as f32; 3], post: [b0 as f32; 3] });
|
||||
}
|
||||
Xf::Group(list) => {
|
||||
if inverse {
|
||||
list.iter().rev().for_each(|x| config_xf_ops(x, true, ops));
|
||||
} else {
|
||||
list.iter().for_each(|x| config_xf_ops(x, false, ops));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The ops of a custom config's conversion from space `a` to space `z` (as `Config::convert`).
|
||||
fn config_ops(a: &crate::ocio_config::ConfigSpace, z: &crate::ocio_config::ConfigSpace, ops: &mut Vec<ColorOp>) {
|
||||
if a.is_data || z.is_data || a.name == z.name {
|
||||
return;
|
||||
}
|
||||
match (&a.to_ref, &a.from_ref) {
|
||||
(Some(x), _) => config_xf_ops(x, false, ops),
|
||||
(None, Some(x)) => config_xf_ops(x, true, ops),
|
||||
_ => {}
|
||||
}
|
||||
match (&z.from_ref, &z.to_ref) {
|
||||
(Some(x), _) => config_xf_ops(x, false, ops),
|
||||
(None, Some(x)) => config_xf_ops(x, true, ops),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// How a colour program meets premultiplied pixels.
|
||||
|
|
@ -912,8 +994,8 @@ fn xform_ops(x: &Xform, ops: &mut Vec<ColorOp>) {
|
|||
}
|
||||
|
||||
/// Effect `id`'s per-pixel transform as a list of [`ColorOp`]s that reproduces the CPU effect
|
||||
/// (empty = the layer passes through). `None` when it has no such form (a custom OCIO config,
|
||||
/// another effect).
|
||||
/// (empty = the layer passes through; custom OCIO configs compile their transforms too). `None`
|
||||
/// for another effect.
|
||||
pub fn color_program(id: &str, ctx: &EffectCtx) -> Option<(Vec<ColorOp>, Straight)> {
|
||||
let pr = ctx.params;
|
||||
let mut ops = vec![];
|
||||
|
|
@ -926,7 +1008,16 @@ pub fn color_program(id: &str, ctx: &EffectCtx) -> Option<(Vec<ColorOp>, Straigh
|
|||
}
|
||||
"ec.color.ociocolorspace" => {
|
||||
if pr.e("config") == 1 {
|
||||
return custom_config(ctx).is_none().then_some((ops, Straight::Unpremul));
|
||||
// A custom config (one that can't be read, or names unknown spaces: the pixels
|
||||
// pass through).
|
||||
if let Some(cfg) = custom_config(ctx)
|
||||
&& let (Some(a), Some(z)) =
|
||||
(custom_space(&cfg, pr.s("sourceName"), pr.e("source")), custom_space(&cfg, pr.s("destinationName"), pr.e("destination")))
|
||||
{
|
||||
let (a, z) = if pr.e("direction") == 1 { (z, a) } else { (a, z) };
|
||||
config_ops(a, z, &mut ops);
|
||||
}
|
||||
return Some((ops, Straight::Unpremul));
|
||||
}
|
||||
let (a, z) = (space_at(pr.e("source")), space_at(pr.e("destination")));
|
||||
let (a, z) = if pr.e("direction") == 1 { (z, a) } else { (a, z) };
|
||||
|
|
@ -942,7 +1033,17 @@ pub fn color_program(id: &str, ctx: &EffectCtx) -> Option<(Vec<ColorOp>, Straigh
|
|||
}
|
||||
"ec.color.ociodisplay" => {
|
||||
if pr.e("config") == 1 {
|
||||
return custom_config(ctx).is_none().then_some((ops, Straight::Unpremul));
|
||||
if let Some(cfg) = custom_config(ctx) {
|
||||
let display = DISPLAYS.get(pr.e("display") as usize).map_or("", |d| d.0);
|
||||
let view = VIEWS.get(pr.e("view") as usize).copied().unwrap_or("");
|
||||
let dn = if pr.s("displayName").is_empty() { display } else { pr.s("displayName") };
|
||||
let vn = if pr.s("viewName").is_empty() { view } else { pr.s("viewName") };
|
||||
if let (Some(a), Some(z)) = (custom_space(&cfg, pr.s("sourceName"), pr.e("source")), cfg.view_space(dn, vn)) {
|
||||
let (a, z) = if pr.e("direction") == 1 { (z, a) } else { (a, z) };
|
||||
config_ops(a, z, &mut ops);
|
||||
}
|
||||
}
|
||||
return Some((ops, Straight::Unpremul));
|
||||
}
|
||||
let input = space_at(pr.e("source"));
|
||||
let (display, view, inverse) = (pr.e("display") as usize, pr.e("view") as usize, pr.e("direction") == 1);
|
||||
|
|
@ -974,17 +1075,7 @@ pub fn color_program(id: &str, ctx: &EffectCtx) -> Option<(Vec<ColorOp>, Straigh
|
|||
1 => 1,
|
||||
_ => 2,
|
||||
};
|
||||
let inverse = pr.e("direction") == 1;
|
||||
match &*x {
|
||||
FileXform::Cdl(list) => {
|
||||
let id = pr.s("cccId");
|
||||
let cdl = list.iter().find(|(i, _)| !id.is_empty() && i == id).or(list.first()).map(|(_, c)| *c).unwrap_or_default();
|
||||
ops.push(ColorOp::Cdl { cdl, clamp: true, inverse });
|
||||
}
|
||||
FileXform::Lut { lut, shaper } => {
|
||||
ops.push(ColorOp::Lut { lut: Arc::new(lut.clone()), shaper: shaper.clone().map(Arc::new), interp, inverse });
|
||||
}
|
||||
}
|
||||
file_ops(&x, interp, pr.e("direction") == 1, pr.s("cccId"), &mut ops);
|
||||
}
|
||||
}
|
||||
"ec.color.ociolook" => {
|
||||
|
|
|
|||
|
|
@ -439,7 +439,7 @@ fn mat_apply(m: &[f64; 16], off: &[f64; 4], c: [f64; 3]) -> [f64; 3] {
|
|||
[0, 1, 2].map(|r| m[r * 4] * c[0] + m[r * 4 + 1] * c[1] + m[r * 4 + 2] * c[2] + off[r])
|
||||
}
|
||||
|
||||
fn mat3_inverse(m: &[f64; 16]) -> Option<[[f64; 3]; 3]> {
|
||||
pub(crate) fn mat3_inverse(m: &[f64; 16]) -> Option<[[f64; 3]; 3]> {
|
||||
let a = [[m[0], m[1], m[2]], [m[4], m[5], m[6]], [m[8], m[9], m[10]]];
|
||||
let det = a[0][0] * (a[1][1] * a[2][2] - a[1][2] * a[2][1]) - a[0][1] * (a[1][0] * a[2][2] - a[1][2] * a[2][0])
|
||||
+ a[0][2] * (a[1][0] * a[2][1] - a[1][1] * a[2][0]);
|
||||
|
|
|
|||
|
|
@ -268,6 +268,8 @@ pub fn sprite_plan(id: &str, ctx: &EffectCtx, b: &Buf) -> Option<SpritePlan> {
|
|||
crate::sim2::WavePlan::Height { .. } => None,
|
||||
}),
|
||||
"ec.sim.foam" => crate::sim2::foam_plan(ctx, b),
|
||||
"ec.sim.ccparticleworld" => particle_world_plan(ctx, b),
|
||||
"ec.sim.ccparticlesystems2" => Some(particle_systems2_plan(ctx, b)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
|
@ -1059,12 +1061,20 @@ fn pw_phys(ctx: &EffectCtx) -> Phys {
|
|||
}
|
||||
}
|
||||
|
||||
fn particle_world(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
fn particle_world(ctx: &EffectCtx, b: Buf) -> Buf {
|
||||
match particle_world_plan(ctx, &b) {
|
||||
Some(plan) => plan.finish(b),
|
||||
None => b,
|
||||
}
|
||||
}
|
||||
|
||||
/// CC Particle World's sprites (far to near), `None` before anything is born.
|
||||
fn particle_world_plan(ctx: &EffectCtx, b: &Buf) -> Option<SpritePlan> {
|
||||
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;
|
||||
return None;
|
||||
}
|
||||
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));
|
||||
|
|
@ -1097,20 +1107,24 @@ fn particle_world(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
.collect();
|
||||
list.sort_by(|a, b| b.0.total_cmp(&a.0));
|
||||
let sprites: Vec<Sprite> = list.into_iter().map(|(_, s)| s).collect();
|
||||
let mode = pr.e("particle/transferMode");
|
||||
let fx = splat(b.img.width, b.img.height, &sprites, if mode == 2 { Acc::Add } else { Acc::Over });
|
||||
b.img = combine(
|
||||
&b.img,
|
||||
&fx,
|
||||
match mode {
|
||||
Some(particle_sprites(sprites, pr.e("particle/transferMode")))
|
||||
}
|
||||
|
||||
/// The CC particle systems' sprites with their Transfer Mode (Composite, Screen, Add, Black
|
||||
/// Background).
|
||||
fn particle_sprites(sprites: Vec<Sprite>, mode: u32) -> SpritePlan {
|
||||
SpritePlan {
|
||||
sprites,
|
||||
tints: vec![],
|
||||
acc: if mode == 2 { Acc::Add } else { Acc::Over },
|
||||
post: Post::Combine(match mode {
|
||||
0 => 0,
|
||||
1 => 1,
|
||||
2 => 2,
|
||||
3 => 5,
|
||||
_ => 0,
|
||||
},
|
||||
);
|
||||
b
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ CC Particle Systems II
|
||||
|
|
@ -1143,7 +1157,12 @@ fn ps2_phys(ctx: &EffectCtx) -> Phys {
|
|||
}
|
||||
}
|
||||
|
||||
fn particle_systems2(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
fn particle_systems2(ctx: &EffectCtx, b: Buf) -> Buf {
|
||||
particle_systems2_plan(ctx, &b).finish(b)
|
||||
}
|
||||
|
||||
/// CC Particle Systems II's sprites.
|
||||
fn particle_systems2_plan(ctx: &EffectCtx, b: &Buf) -> SpritePlan {
|
||||
let pr = ctx.params;
|
||||
let lh = ctx.layer_size[1] as f32;
|
||||
let unit = lh.max(1.0);
|
||||
|
|
@ -1167,20 +1186,7 @@ fn particle_systems2(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
type_sprite(kind, bx as f32, by as f32, r, c, q.v[0] * unit * s, q.v[1] * unit * s, (q.id % 628) as f32 * 0.01 + q.age * 2.0)
|
||||
})
|
||||
.collect();
|
||||
let mode = pr.e("particle/transferMode");
|
||||
let fx = splat(b.img.width, b.img.height, &sprites, if mode == 2 { Acc::Add } else { Acc::Over });
|
||||
b.img = combine(
|
||||
&b.img,
|
||||
&fx,
|
||||
match mode {
|
||||
0 => 0,
|
||||
1 => 1,
|
||||
2 => 2,
|
||||
3 => 5,
|
||||
_ => 0,
|
||||
},
|
||||
);
|
||||
b
|
||||
particle_sprites(sprites, pr.e("particle/transferMode"))
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ CC Mr. Mercury
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ use rayon::prelude::*;
|
|||
|
||||
use crate::card3d::{Lighting, Proj};
|
||||
use crate::generate::value_noise;
|
||||
use crate::sim::{Acc, Shape, Sprite, h, hs, pct, pt, raster, spec, splat};
|
||||
use crate::sim::{Acc, Post, Shape, Sprite, SpritePlan, h, hs, pct, pt, raster, spec, splat};
|
||||
use crate::util::{Plane, SimCache, gauss_plane, layer_or_self, params_key, unpremul};
|
||||
use crate::{Buf, EffectCtx, EffectSpec, col, num, p, popup, slider};
|
||||
|
||||
|
|
@ -146,7 +146,10 @@ impl PiecePlan {
|
|||
pub fn piece_plan(id: &str, ctx: &EffectCtx, b: &Buf) -> Option<PiecePlan> {
|
||||
match id {
|
||||
"ec.sim.carddance" => Some(card_dance_plan(ctx, b)),
|
||||
"ec.sim.shatter" if ctx.params.e("view") == 0 => shatter_impl(ctx, b, true).err(),
|
||||
"ec.sim.shatter" if ctx.params.e("view") == 0 => match shatter_impl(ctx, b) {
|
||||
ShatterPlan::Pieces(plan) => Some(plan),
|
||||
ShatterPlan::Wire(_) => None,
|
||||
},
|
||||
"ec.transition.cardwipe" => crate::transition::card_wipe_plan(ctx, b),
|
||||
_ => None,
|
||||
}
|
||||
|
|
@ -290,7 +293,12 @@ pub(crate) fn sort_far_first(pieces: &mut [Piece]) {
|
|||
|
||||
// ------------------------------------------------------------------ CC Ball Action
|
||||
|
||||
fn ball_action(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
fn ball_action(ctx: &EffectCtx, b: Buf) -> Buf {
|
||||
ball_action_plan(ctx, &b).finish(b)
|
||||
}
|
||||
|
||||
/// CC Ball Action's balls (far to near), coloured by the layer's pixels.
|
||||
fn ball_action_plan(ctx: &EffectCtx, b: &Buf) -> SpritePlan {
|
||||
let pr = ctx.params;
|
||||
let scatter = pr.f("scatter") as f32;
|
||||
let axis = pr.e("rotationAxis");
|
||||
|
|
@ -307,7 +315,7 @@ fn ball_action(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
let ny = (lh / spacing).ceil().max(1.0) as u32;
|
||||
let d = 2.0 * lw.max(lh) as f64;
|
||||
let (cx, cy) = (lw as f64 * 0.5, lh as f64 * 0.5);
|
||||
let src = b.img.clone();
|
||||
let src = &b.img;
|
||||
let ax: [f64; 3] = match axis {
|
||||
0 => [1.0, 0.0, 0.0],
|
||||
1 => [0.0, 1.0, 0.0],
|
||||
|
|
@ -353,17 +361,24 @@ fn ball_action(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
.collect();
|
||||
balls.sort_by(|a, b| b.0.total_cmp(&a.0));
|
||||
let sprites: Vec<Sprite> = balls.into_iter().map(|(_, s)| s).collect();
|
||||
b.img = splat(src.width, src.height, &sprites, Acc::Over);
|
||||
b
|
||||
SpritePlan { sprites, tints: vec![], acc: Acc::Over, post: Post::Replace }
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ CC Pixel Polly
|
||||
|
||||
fn pixel_polly(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
fn pixel_polly(ctx: &EffectCtx, b: Buf) -> Buf {
|
||||
match pixel_polly_plan(ctx, &b) {
|
||||
Some(plan) => plan.finish(b),
|
||||
None => b,
|
||||
}
|
||||
}
|
||||
|
||||
/// CC Pixel Polly's pieces, `None` before Start Time (the layer as is).
|
||||
fn pixel_polly_plan(ctx: &EffectCtx, b: &Buf) -> Option<PiecePlan> {
|
||||
let pr = ctx.params;
|
||||
let t = ctx.time - pr.f("startTime");
|
||||
if t <= 0.0 {
|
||||
return b;
|
||||
return None;
|
||||
}
|
||||
let force = pr.f("force");
|
||||
let gravity = pr.f("gravity");
|
||||
|
|
@ -383,7 +398,7 @@ fn pixel_polly(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
let tri = object <= 1;
|
||||
let textured = object == 1 || object == 3;
|
||||
let maxd = lw.max(lh);
|
||||
let src = b.img.clone();
|
||||
let src = &b.img;
|
||||
let per = if tri { 2 } else { 1 };
|
||||
let mut pieces: Vec<Piece> = (0..nx * ny * per)
|
||||
.into_par_iter()
|
||||
|
|
@ -425,21 +440,26 @@ fn pixel_polly(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
if depth_sort {
|
||||
sort_far_first(&mut pieces);
|
||||
}
|
||||
let mut out = Image::new(src.width, src.height);
|
||||
draw_pieces(&mut out, &pieces, &src, None);
|
||||
b.img = out;
|
||||
b
|
||||
Some(PiecePlan { pieces, front: PieceTex::Layer, back: None })
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ CC Scatterize
|
||||
|
||||
fn scatterize(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
fn scatterize(ctx: &EffectCtx, b: Buf) -> Buf {
|
||||
match scatterize_plan(ctx, &b) {
|
||||
Some(plan) => plan.finish(b),
|
||||
None => b,
|
||||
}
|
||||
}
|
||||
|
||||
/// CC Scatterize's pixel dots, `None` when nothing moves (the layer as is).
|
||||
fn scatterize_plan(ctx: &EffectCtx, b: &Buf) -> Option<SpritePlan> {
|
||||
let pr = ctx.params;
|
||||
let scatter = pr.f("scatter") as f32;
|
||||
let right = pr.f("rightTwist").to_radians();
|
||||
let left = pr.f("leftTwist").to_radians();
|
||||
if scatter == 0.0 && right == 0.0 && left == 0.0 {
|
||||
return b;
|
||||
return None;
|
||||
}
|
||||
let add = pr.e("transferMode") == 1;
|
||||
let seed = ctx.seed.wrapping_mul(0x7feb352d);
|
||||
|
|
@ -471,8 +491,29 @@ fn scatterize(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
Some(Sprite::new(sx as f32, sy as f32, 0.6 * persp as f32, [c[0], c[1], c[2], a], Shape::Disc))
|
||||
})
|
||||
.collect();
|
||||
b.img = splat(w, hh, &sprites, if add { Acc::Add } else { Acc::Over });
|
||||
b
|
||||
Some(SpritePlan { sprites, tints: vec![], acc: if add { Acc::Add } else { Acc::Over }, post: Post::Replace })
|
||||
}
|
||||
|
||||
/// What a particle effect that reads the layer's pixels draws this frame (CC Ball Action, CC
|
||||
/// Pixel Polly, CC Scatterize, Shatter's wireframe views), for layer `b`; `None` for other
|
||||
/// effects or a frame showing the layer as is.
|
||||
pub enum PixelPlan {
|
||||
Sprites(SpritePlan),
|
||||
Pieces(PiecePlan),
|
||||
}
|
||||
|
||||
/// The [`PixelPlan`] of effect `id` for layer `b` (its pixels are read).
|
||||
pub fn pixel_plan(id: &str, ctx: &EffectCtx, b: &Buf) -> Option<PixelPlan> {
|
||||
match id {
|
||||
"ec.sim.ccballaction" => Some(PixelPlan::Sprites(ball_action_plan(ctx, b))),
|
||||
"ec.sim.ccpixelpolly" => pixel_polly_plan(ctx, b).map(PixelPlan::Pieces),
|
||||
"ec.sim.ccscatterize" => scatterize_plan(ctx, b).map(PixelPlan::Sprites),
|
||||
"ec.sim.shatter" => Some(match shatter_impl(ctx, b) {
|
||||
ShatterPlan::Pieces(plan) => PixelPlan::Pieces(plan),
|
||||
ShatterPlan::Wire(plan) => PixelPlan::Sprites(plan),
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ Card Dance
|
||||
|
|
@ -686,14 +727,19 @@ fn map_key(c: [f32; 4]) -> u32 {
|
|||
}
|
||||
|
||||
fn shatter(ctx: &EffectCtx, b: Buf) -> Buf {
|
||||
match shatter_impl(ctx, &b, false) {
|
||||
Ok(img) => Buf { img, ..b },
|
||||
Err(plan) => plan.finish(b),
|
||||
match shatter_impl(ctx, &b) {
|
||||
ShatterPlan::Pieces(plan) => plan.finish(b),
|
||||
ShatterPlan::Wire(plan) => plan.finish(b),
|
||||
}
|
||||
}
|
||||
|
||||
/// Shatter: the frame (`Ok`), or with `plan_only` in the Rendered view its pieces (`Err`).
|
||||
fn shatter_impl(ctx: &EffectCtx, b: &Buf, plan_only: bool) -> Result<Image, PiecePlan> {
|
||||
/// What Shatter draws: the pieces (Rendered view) or the wireframe lines.
|
||||
enum ShatterPlan {
|
||||
Pieces(PiecePlan),
|
||||
Wire(SpritePlan),
|
||||
}
|
||||
|
||||
fn shatter_impl(ctx: &EffectCtx, b: &Buf) -> ShatterPlan {
|
||||
let pr = ctx.params;
|
||||
let view = pr.e("view");
|
||||
let render = pr.e("render");
|
||||
|
|
@ -770,7 +816,6 @@ fn shatter_impl(ctx: &EffectCtx, b: &Buf, plan_only: bool) -> Result<Image, Piec
|
|||
e.1 += 1.0;
|
||||
}
|
||||
let g3 = [gdir.sin() * ginc.cos() * gravity * unit, -gdir.cos() * ginc.cos() * gravity * unit, ginc.sin() * gravity * unit];
|
||||
let src = b.img.clone();
|
||||
let mut pieces: Vec<Piece> = polys
|
||||
.par_iter()
|
||||
.zip(keys.par_iter())
|
||||
|
|
@ -902,13 +947,8 @@ fn shatter_impl(ctx: &EffectCtx, b: &Buf, plan_only: bool) -> Result<Image, Piec
|
|||
let tex = |i: &Option<Image>| i.clone().map(PieceTex::Image);
|
||||
let front_t = tex(&front.img).unwrap_or(PieceTex::Layer);
|
||||
let back_t = tex(&backf.img).unwrap_or_else(|| front_t.clone());
|
||||
let plan = PiecePlan { pieces, front: front_t, back: Some(back_t) };
|
||||
if plan_only {
|
||||
return Err(plan);
|
||||
}
|
||||
return Ok(plan.finish(Buf { img: src, offset: b.offset, scale: b.scale }).img);
|
||||
return ShatterPlan::Pieces(PiecePlan { pieces, front: front_t, back: Some(back_t) });
|
||||
}
|
||||
let out;
|
||||
{
|
||||
// Wireframes: piece outlines (front view: at rest; otherwise animated), plus forces.
|
||||
let animated = view == 2 || view == 4;
|
||||
|
|
@ -948,9 +988,8 @@ fn shatter_impl(ctx: &EffectCtx, b: &Buf, plan_only: bool) -> Result<Image, Piec
|
|||
}
|
||||
}
|
||||
}
|
||||
out = splat(src.width, src.height, &lines, Acc::Over);
|
||||
ShatterPlan::Wire(SpritePlan { sprites: lines, tints: vec![], acc: Acc::Over, post: Post::Replace })
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ Caustics
|
||||
|
|
@ -1516,22 +1555,97 @@ fn foam_pop_age(id: u32, lifespan: f32, frailty: f32, seed: u32) -> f32 {
|
|||
/// neighbours away at Pop Velocity. Rendered bubbles use a built-in texture or the Bubble
|
||||
/// Texture Layer, oriented by Bubble Orientation, and reflect the Environment Map.
|
||||
fn foam(ctx: &EffectCtx, b: Buf) -> Buf {
|
||||
match foam_impl(ctx, &b, false) {
|
||||
Ok(img) => Buf { img, ..b },
|
||||
Err(plan) => plan.finish(b),
|
||||
}
|
||||
foam_full_plan(ctx, &b).finish(b)
|
||||
}
|
||||
|
||||
/// Foam's bubbles as sprites, `None` when it draws more than sprites (a User Defined bubble
|
||||
/// texture, an Environment Map reflection or the Draft + Flow Map view's flow map).
|
||||
pub(crate) fn foam_plan(ctx: &EffectCtx, b: &Buf) -> Option<crate::sim::SpritePlan> {
|
||||
if !crate::catalog::gpu_supported("ec.sim.foam", ctx) {
|
||||
return None;
|
||||
}
|
||||
foam_impl(ctx, b, true).err()
|
||||
let plan = foam_full_plan(ctx, b);
|
||||
(plan.user.is_none() && plan.env.is_none() && plan.flow.is_none()).then_some(plan.sprites)
|
||||
}
|
||||
|
||||
fn foam_impl(ctx: &EffectCtx, b: &Buf, plan_only: bool) -> Result<Image, crate::sim::SpritePlan> {
|
||||
/// A bubble disc filled by Foam's User Defined texture (buffer px; `rot` radians).
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct FoamDisc {
|
||||
pub x: f64,
|
||||
pub y: f64,
|
||||
pub r: f32,
|
||||
pub rot: f32,
|
||||
pub fade: f32,
|
||||
}
|
||||
|
||||
/// Foam's Environment Map reflection: the map fitted to the buffer, Reflection Convergence and
|
||||
/// Strength, and the bubble discs reflecting it (`rot` and `fade` unused).
|
||||
pub struct FoamEnv {
|
||||
pub img: Image,
|
||||
pub conv: f64,
|
||||
pub strength: f32,
|
||||
pub discs: Vec<FoamDisc>,
|
||||
}
|
||||
|
||||
/// What Foam draws this frame over a transparent frame of the layer's size: the bubble sprites,
|
||||
/// then the User Defined texture's discs, the Environment Map's reflections and (Draft + Flow
|
||||
/// Map) the flow map under it all, each [`foam_disc`]-composited in bubble order.
|
||||
pub struct FoamPlan {
|
||||
pub sprites: crate::sim::SpritePlan,
|
||||
pub user: Option<(crate::LayerPixels, Vec<FoamDisc>)>,
|
||||
pub env: Option<FoamEnv>,
|
||||
/// The flow map fitted to the buffer.
|
||||
pub flow: Option<Image>,
|
||||
}
|
||||
|
||||
impl FoamPlan {
|
||||
pub fn finish(&self, b: Buf) -> Buf {
|
||||
let mut out = splat(b.img.width, b.img.height, &self.sprites.sprites, self.sprites.acc);
|
||||
// User Defined: the texture layer fills each bubble's disc.
|
||||
if let Some((tex, discs)) = &self.user {
|
||||
for d in discs {
|
||||
let (rot, fade) = (d.rot, d.fade);
|
||||
foam_disc(&mut out, d.x, d.y, d.r as f64, |u, v| {
|
||||
// u, v in −1..1 across the bubble, rotated into the texture.
|
||||
let (sr, cr) = rot.sin_cos();
|
||||
let (tu, tv) = (u as f32 * cr + v as f32 * sr, -u as f32 * sr + v as f32 * cr);
|
||||
let (x, y) = ((tu as f64 * 0.5 + 0.5) * tex.size[0], (tv as f64 * 0.5 + 0.5) * tex.size[1]);
|
||||
let px = tex.buf.img.sample_bilinear(x * tex.buf.scale + tex.buf.offset[0], y * tex.buf.scale + tex.buf.offset[1]);
|
||||
px.map(|c| c * fade)
|
||||
});
|
||||
}
|
||||
}
|
||||
// Environment Map: each bubble reflects the map along its sphere normal (Reflection
|
||||
// Convergence pulls the reflection towards the bubble's own spot of the map).
|
||||
if let Some(env) = &self.env {
|
||||
let (conv, strength) = (env.conv, env.strength);
|
||||
let (w, h) = (b.img.width as f64, b.img.height as f64);
|
||||
for d in &env.discs {
|
||||
let (bx, by) = (d.x, d.y);
|
||||
foam_disc(&mut out, bx, by, d.r as f64, |u, v| {
|
||||
// Sphere normal; the reflection looks across the whole map, or (convergence 1)
|
||||
// just behind the bubble.
|
||||
let (ex, ey) =
|
||||
if conv >= 1.0 { (bx, by) } else { (bx * conv + (u * 0.5 + 0.5) * w * (1.0 - conv), by * conv + (v * 0.5 + 0.5) * h * (1.0 - conv)) };
|
||||
let e = env.img.sample_bilinear(ex, ey);
|
||||
let rim = ((u * u + v * v) as f32).min(1.0);
|
||||
let k = strength * (0.3 + 0.7 * rim);
|
||||
[e[0] * k, e[1] * k, e[2] * k, 0.0]
|
||||
});
|
||||
}
|
||||
}
|
||||
// Draft + Flow Map shows the flow map under the bubbles.
|
||||
if let Some(fm) = &self.flow {
|
||||
out.data.par_iter_mut().zip(fm.data.par_iter()).for_each(|(o, f)| {
|
||||
let k = 1.0 - o[3];
|
||||
for c in 0..4 {
|
||||
o[c] += f[c] * 0.5 * k;
|
||||
}
|
||||
});
|
||||
}
|
||||
Buf { img: out, ..b }
|
||||
}
|
||||
}
|
||||
|
||||
/// Foam's [`FoamPlan`] for layer geometry `b` (its size; its pixels are not read).
|
||||
pub fn foam_full_plan(ctx: &EffectCtx, b: &Buf) -> FoamPlan {
|
||||
let pr = ctx.params;
|
||||
let (lw, lh) = (ctx.layer_size[0] as f32, ctx.layer_size[1] as f32);
|
||||
let unit = lw * 0.01;
|
||||
|
|
@ -1754,62 +1868,34 @@ fn foam_impl(ctx: &EffectCtx, b: &Buf, plan_only: bool) -> Result<Image, crate::
|
|||
})
|
||||
.collect();
|
||||
let acc = if blend == 0 && view == 2 { Acc::Add } else { Acc::Over };
|
||||
if plan_only {
|
||||
return Err(crate::sim::SpritePlan { sprites, tints: vec![], acc, post: crate::sim::Post::Replace });
|
||||
}
|
||||
let mut out = splat(b.img.width, b.img.height, &sprites, acc);
|
||||
// User Defined: the texture layer fills each bubble's disc.
|
||||
if let Some(tex) = &user_tex {
|
||||
for q in &order {
|
||||
let (bx, by, r, rot) = place(q);
|
||||
let fade = ((foam_pop_age(q.id, lifespan, frailty, seed) - q.age) / 5.0).clamp(0.0, 1.0);
|
||||
foam_disc(&mut out, bx, by, r as f64, |u, v| {
|
||||
// u, v in −1..1 across the bubble, rotated into the texture.
|
||||
let (sr, cr) = rot.sin_cos();
|
||||
let (tu, tv) = (u as f32 * cr + v as f32 * sr, -u as f32 * sr + v as f32 * cr);
|
||||
let (x, y) = ((tu as f64 * 0.5 + 0.5) * tex.size[0], (tv as f64 * 0.5 + 0.5) * tex.size[1]);
|
||||
let px = tex.buf.img.sample_bilinear(x * tex.buf.scale + tex.buf.offset[0], y * tex.buf.scale + tex.buf.offset[1]);
|
||||
px.map(|c| c * fade)
|
||||
});
|
||||
}
|
||||
}
|
||||
// Environment Map: each bubble reflects the map along its sphere normal (Reflection
|
||||
// Convergence pulls the reflection towards the bubble's own spot of the map).
|
||||
let disc = |q: &&FoamBubble| {
|
||||
let (x, y, r, rot) = place(q);
|
||||
let fade = ((foam_pop_age(q.id, lifespan, frailty, seed) - q.age) / 5.0).clamp(0.0, 1.0);
|
||||
FoamDisc { x, y, r, rot, fade }
|
||||
};
|
||||
let user = user_tex.map(|tex| (tex, order.iter().map(disc).collect()));
|
||||
let strength = pr.f("rendering/reflectionStrength").clamp(0.0, 1.0) as f32;
|
||||
if view == 2
|
||||
let env = if view == 2
|
||||
&& strength > 0.0
|
||||
&& let Some(env) = ctx.layer_param("rendering/environmentMap", true)
|
||||
{
|
||||
let env_img = crate::util::fit_layer(ctx, b, &env, true);
|
||||
let conv = pr.f("rendering/reflectionConvergence").clamp(0.0, 1.0);
|
||||
let (w, h) = (b.img.width as f64, b.img.height as f64);
|
||||
for q in &order {
|
||||
let (bx, by, r, _) = place(q);
|
||||
foam_disc(&mut out, bx, by, r as f64, |u, v| {
|
||||
// Sphere normal; the reflection looks across the whole map, or (convergence 1)
|
||||
// just behind the bubble.
|
||||
let (ex, ey) =
|
||||
if conv >= 1.0 { (bx, by) } else { (bx * conv + (u * 0.5 + 0.5) * w * (1.0 - conv), by * conv + (v * 0.5 + 0.5) * h * (1.0 - conv)) };
|
||||
let e = env_img.sample_bilinear(ex, ey);
|
||||
let rim = ((u * u + v * v) as f32).min(1.0);
|
||||
let k = strength * (0.3 + 0.7 * rim);
|
||||
[e[0] * k, e[1] * k, e[2] * k, 0.0]
|
||||
});
|
||||
}
|
||||
}
|
||||
// Draft + Flow Map shows the flow map under the bubbles.
|
||||
if view == 1
|
||||
Some(FoamEnv {
|
||||
img: crate::util::fit_layer(ctx, b, &env, true),
|
||||
conv: pr.f("rendering/reflectionConvergence").clamp(0.0, 1.0),
|
||||
strength,
|
||||
discs: order.iter().map(disc).collect(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let flow = if view == 1
|
||||
&& let Some(lp) = &flow
|
||||
{
|
||||
let fm = crate::util::fit_layer(ctx, b, lp, true);
|
||||
out.data.par_iter_mut().zip(fm.data.par_iter()).for_each(|(o, f)| {
|
||||
let k = 1.0 - o[3];
|
||||
for c in 0..4 {
|
||||
o[c] += f[c] * 0.5 * k;
|
||||
}
|
||||
});
|
||||
}
|
||||
Ok(out)
|
||||
Some(crate::util::fit_layer(ctx, b, lp, true))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
FoamPlan { sprites: crate::sim::SpritePlan { sprites, tints: vec![], acc, post: crate::sim::Post::Replace }, user, env, flow }
|
||||
}
|
||||
|
||||
/// Add `f(u, v)` (premultiplied; u, v ∈ −1..1 across the disc) over the disc of radius `r` at
|
||||
|
|
|
|||
|
|
@ -716,7 +716,12 @@ pub fn playground_state(ctx: &EffectCtx) -> Option<Vec<crate::psim::SimParticle>
|
|||
)
|
||||
}
|
||||
|
||||
fn particle_playground(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
fn particle_playground(ctx: &EffectCtx, b: Buf) -> Buf {
|
||||
playground_plan(ctx, &b).finish(b)
|
||||
}
|
||||
|
||||
/// Particle Playground's [`PgPlan`] for buffer geometry `b` (its pixels are not read).
|
||||
pub fn playground_plan(ctx: &EffectCtx, b: &Buf) -> PgPlan {
|
||||
let pr = ctx.params;
|
||||
let (lw, lh) = (ctx.layer_size[0] as f32, ctx.layer_size[1] as f32);
|
||||
let pg = pg_of(ctx);
|
||||
|
|
@ -825,7 +830,6 @@ fn particle_playground(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
let offset_type = pr.e("layerMap/timeOffsetType");
|
||||
let offset = pr.f("layerMap/timeOffset");
|
||||
let fps = ctx.fps();
|
||||
let mut frames: std::collections::HashMap<i64, Option<crate::LayerPixels>> = std::collections::HashMap::new();
|
||||
let mut sprites: Vec<Sprite> = vec![];
|
||||
let mut blits: Vec<(PgParticle, i64)> = vec![];
|
||||
for q in &parts {
|
||||
|
|
@ -869,30 +873,64 @@ fn particle_playground(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
None => sprites.push(Sprite::new(x as f32, y as f32, q.r * sc * s, q.c, Shape::Disc)),
|
||||
}
|
||||
}
|
||||
let mut fx = splat(b.img.width, b.img.height, &sprites, Acc::Over);
|
||||
let mut plan = PgPlan { sprites: SpritePlan { sprites, tints: vec![], acc: Acc::Over, post: Post::Combine(5) }, blits: vec![], frames: vec![] };
|
||||
if let (Some(id), Some(host)) = (map_id, ctx.env.host) {
|
||||
let me = pr.get(&crate::layer_source_id("layerMap/layerMapLayer")).is_none_or(|v| v.as_enum() != 0);
|
||||
// Fetched frames by frame number: their index in `plan.frames` (`None` = unavailable).
|
||||
let mut frames: std::collections::HashMap<i64, Option<usize>> = std::collections::HashMap::new();
|
||||
for (q, frame) in &blits {
|
||||
if !frames.contains_key(frame) && frames.len() >= 32 {
|
||||
let at = if !frames.contains_key(frame) && frames.len() >= 32 {
|
||||
// Too many distinct times: reuse the nearest fetched frame.
|
||||
if let Some(k) = frames.keys().min_by_key(|k| (*k - frame).abs()).copied()
|
||||
&& let Some(Some(lp)) = frames.get(&k)
|
||||
{
|
||||
let (x, y) = b.to_px([q.p[0] as f64, q.p[1] as f64]);
|
||||
blit_layer(&mut fx, lp, x, y, q.angle as f64, [q.scale[0] as f64, q.scale[1] as f64], b.scale, q.c[3]);
|
||||
match frames.keys().min_by_key(|k| (*k - frame).abs()).copied() {
|
||||
Some(k) => frames.get(&k).copied().flatten(),
|
||||
None => None,
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let lp = frames.entry(*frame).or_insert_with(|| host.layer_at(id, *frame as f64 / fps, me));
|
||||
if let Some(lp) = lp {
|
||||
} else {
|
||||
*frames.entry(*frame).or_insert_with(|| {
|
||||
let lp = host.layer_at(id, *frame as f64 / fps, me)?;
|
||||
plan.frames.push(lp);
|
||||
Some(plan.frames.len() - 1)
|
||||
})
|
||||
};
|
||||
if let Some(i) = at {
|
||||
let (x, y) = b.to_px([q.p[0] as f64, q.p[1] as f64]);
|
||||
blit_layer(&mut fx, lp, x, y, q.angle as f64, [q.scale[0] as f64, q.scale[1] as f64], b.scale, q.c[3]);
|
||||
plan.blits.push(PgBlit { frame: i, x, y, angle: q.angle as f64, scale: [q.scale[0] as f64, q.scale[1] as f64], alpha: q.c[3] });
|
||||
}
|
||||
}
|
||||
}
|
||||
// Particles replace the layer's own pixels.
|
||||
b.img = combine(&b.img, &fx, 5);
|
||||
b
|
||||
plan
|
||||
}
|
||||
|
||||
/// A Layer Map particle: frame `frame` of [`PgPlan::frames`] centred on (`x`, `y`) buffer px,
|
||||
/// rotated `angle` degrees, scaled and faded to `alpha`.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct PgBlit {
|
||||
pub frame: usize,
|
||||
pub x: f64,
|
||||
pub y: f64,
|
||||
pub angle: f64,
|
||||
pub scale: [f64; 2],
|
||||
pub alpha: f32,
|
||||
}
|
||||
|
||||
/// What Particle Playground draws this frame: the dot and text particles as sprites, then the
|
||||
/// Layer Map particles over them in order; the result replaces the layer's own pixels.
|
||||
pub struct PgPlan {
|
||||
pub sprites: SpritePlan,
|
||||
pub blits: Vec<PgBlit>,
|
||||
pub frames: Vec<crate::LayerPixels>,
|
||||
}
|
||||
|
||||
impl PgPlan {
|
||||
pub fn finish(&self, mut b: Buf) -> Buf {
|
||||
let mut fx = splat(b.img.width, b.img.height, &self.sprites.sprites, self.sprites.acc);
|
||||
for q in &self.blits {
|
||||
blit_layer(&mut fx, &self.frames[q.frame], q.x, q.y, q.angle, q.scale, b.scale, q.alpha);
|
||||
}
|
||||
// Particles replace the layer's own pixels.
|
||||
b.img = combine(&b.img, &fx, 5);
|
||||
b
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Hair
|
||||
|
|
|
|||
|
|
@ -680,18 +680,22 @@ fn fetch_layer(ctx: &EffectCtx, id: &str, times: &[f64], grid: &Buf) -> Option<V
|
|||
)
|
||||
}
|
||||
|
||||
/// One output instant of a Timewarp without a Matte Layer: a fetched frame, a cross-fade of two,
|
||||
/// or the in-between frame built along a motion field ([`TimewarpPlan::flows`]).
|
||||
/// One output instant of a Timewarp: a frame, a cross-fade of two, or the in-between frame
|
||||
/// built along a motion field ([`TimewarpPlan::flows`]); with a Matte Layer's Pixel Motion, the
|
||||
/// foreground and background pairs (`[frame a, frame b, flow]`) each built on their own vectors
|
||||
/// and shown as Show asks.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum TwStep {
|
||||
Whole(usize),
|
||||
Mix(usize, usize, f32),
|
||||
Motion { a: usize, b: usize, w: f32, flow: usize },
|
||||
Layered { fg: [usize; 3], bg: [usize; 3], w: f32, show: u32 },
|
||||
}
|
||||
|
||||
/// Timewarp without a Matte Layer, prepared for the GPU compositor (effectcraft-gpu `fx_time`):
|
||||
/// the fetched frames on one grid, the instants across the shutter (with their weights) and the
|
||||
/// motion fields (block matching on the CPU). [`TimewarpPlan::render_cpu`] is the CPU effect.
|
||||
/// Timewarp prepared for the GPU compositor (effectcraft-gpu `fx_time`): the fetched frames on
|
||||
/// one grid (plus the matte-masked ones a Matte Layer needs), the instants across the shutter
|
||||
/// (with their weights) and the motion fields (block matching on the CPU).
|
||||
/// [`TimewarpPlan::render_cpu`] is the CPU effect.
|
||||
pub struct TimewarpPlan {
|
||||
pub grid: Buf,
|
||||
pub frames: Vec<Image>,
|
||||
|
|
@ -710,6 +714,11 @@ impl TimewarpPlan {
|
|||
TwStep::Whole(i) => f[i].clone(),
|
||||
TwStep::Mix(i, j, w) => effectcraft_raster::flow::mix(&f[i], &f[j], w),
|
||||
TwStep::Motion { a, b, w, flow } => timewarp_interpolate(&f[a], &f[b], w, &self.flows[flow], &self.pm),
|
||||
TwStep::Layered { fg, bg, w, show } => {
|
||||
let fgi = timewarp_interpolate(&f[fg[0]], &f[fg[1]], w, &self.flows[fg[2]], &self.pm);
|
||||
let bgi = timewarp_interpolate(&f[bg[0]], &f[bg[1]], w, &self.flows[bg[2]], &self.pm);
|
||||
layered(fgi, bgi, show)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let weights: Vec<f32> = self.steps.iter().map(|(_, w)| *w).collect();
|
||||
|
|
@ -758,48 +767,102 @@ fn tw_split(s: f64, fps: f64) -> (f64, f32) {
|
|||
(f0, ((f - f0) as f32).max(0.0))
|
||||
}
|
||||
|
||||
/// [`TimewarpPlan`] of a Timewarp instance without a Matte Layer. `None` without the effect host
|
||||
/// (the layer passes through) or with a Matte Layer.
|
||||
/// [`TimewarpPlan`] of a Timewarp instance. `None` without the effect host or source frames (the
|
||||
/// layer passes through). With a Matte Layer the frames the instants need (masked by the matte,
|
||||
/// or the matte itself for Show Matte) are prepared here and join [`TimewarpPlan::frames`].
|
||||
pub fn timewarp_plan(ctx: &EffectCtx) -> Option<TimewarpPlan> {
|
||||
ctx.env.host?;
|
||||
if ctx.params.get("matteLayer").and_then(Value::as_layer).is_some() {
|
||||
return None;
|
||||
}
|
||||
// Instants of source time (with weights) across the shutter; each needs the frame at or
|
||||
// before it and the next one.
|
||||
let (instants, times, fps) = timewarp_instants(ctx);
|
||||
let method = ctx.params.e("method");
|
||||
let one = method > 0 && ctx.params.b("tuning/buildFromOneImage");
|
||||
let fr = fetch(ctx, ×)?;
|
||||
let pm = PixelMotion::from(ctx, fr.grid.scale);
|
||||
let mattes = fetch_layer(ctx, "matteLayer", ×, &fr.grid);
|
||||
let warps = fetch_layer(ctx, "warpLayer", ×, &fr.grid);
|
||||
let matte_ch = ctx.params.e("matteChannel");
|
||||
let show = ctx.params.e("show");
|
||||
let idx = |t: f64| times.iter().position(|q| (q - t).abs() < 1e-9).unwrap_or(0);
|
||||
let mut flows: Vec<(i64, Flow)> = vec![];
|
||||
let mut frames = fr.imgs;
|
||||
let add = |frames: &mut Vec<Image>, img: Image| {
|
||||
frames.push(img);
|
||||
frames.len() - 1
|
||||
};
|
||||
// Motion fields: per source frame pair (keyed), or per masked pair (unkeyed).
|
||||
let mut flows: Vec<(Option<i64>, Flow)> = vec![];
|
||||
let mut steps = vec![];
|
||||
for &(s, wgt) in &instants {
|
||||
let (f0, frac) = tw_split(s, fps);
|
||||
let (i0, i1) = (idx(f0 / fps), idx((f0 + 1.0) / fps));
|
||||
let ma = mattes.as_ref().map(|m| matte_of(&m[i0], matte_ch));
|
||||
let mb = mattes.as_ref().map(|m| matte_of(&m[i1], matte_ch));
|
||||
let step = if method == 0 || frac < 1e-4 || (one && method == 1) {
|
||||
// Whole frames (Build From One Image with Frame Mix: the nearest frame).
|
||||
let pick = if method == 0 || frac < 1e-4 { 0.0 } else { frac.round() };
|
||||
TwStep::Whole(if pick < 0.5 { i0 } else { i1 })
|
||||
let i = if pick < 0.5 { i0 } else { i1 };
|
||||
let base = &frames[i];
|
||||
let derived = match (show, &ma, &mb) {
|
||||
(1, Some(m), _) if pick < 0.5 => Some(times_mask(base, m, false)),
|
||||
(2, Some(m), _) if pick < 0.5 => Some(times_mask(base, m, true)),
|
||||
(1, _, Some(m)) => Some(times_mask(base, m, false)),
|
||||
(2, _, Some(m)) => Some(times_mask(base, m, true)),
|
||||
(3, Some(m), _) => Some(matte_image(if pick < 0.5 { m } else { mb.as_ref().unwrap_or(m) }, base)),
|
||||
_ => None,
|
||||
};
|
||||
TwStep::Whole(match derived {
|
||||
Some(img) => add(&mut frames, img),
|
||||
None => i,
|
||||
})
|
||||
} else if method == 1 {
|
||||
TwStep::Mix(i0, i1, frac)
|
||||
match (&ma, &mb, show) {
|
||||
(Some(m0), Some(m1), 3) => {
|
||||
let (a, b) = (matte_image(m0, &frames[i0]), matte_image(m1, &frames[i1]));
|
||||
TwStep::Mix(add(&mut frames, a), add(&mut frames, b), frac)
|
||||
}
|
||||
(Some(m0), Some(m1), 1 | 2) => {
|
||||
let (a, b) = (times_mask(&frames[i0], m0, show == 2), times_mask(&frames[i1], m1, show == 2));
|
||||
TwStep::Mix(add(&mut frames, a), add(&mut frames, b), frac)
|
||||
}
|
||||
_ => TwStep::Mix(i0, i1, frac),
|
||||
}
|
||||
} else {
|
||||
// Pixel Motion: vectors from the warp layer if any, else from the layer.
|
||||
let key = f0 as i64;
|
||||
let flow = match flows.iter().position(|(k, _)| *k == key) {
|
||||
let flow = match flows.iter().position(|(k, _)| *k == Some(key)) {
|
||||
Some(k) => k,
|
||||
None => {
|
||||
let f = match &warps {
|
||||
Some(w) => timewarp_flow(&w[i0], &w[i1], &pm),
|
||||
None => timewarp_flow(&fr.imgs[i0], &fr.imgs[i1], &pm),
|
||||
None => timewarp_flow(&frames[i0], &frames[i1], &pm),
|
||||
};
|
||||
flows.push((key, f));
|
||||
flows.push((Some(key), f));
|
||||
flows.len() - 1
|
||||
}
|
||||
};
|
||||
TwStep::Motion { a: i0, b: i1, w: frac, flow }
|
||||
match (&ma, &mb) {
|
||||
(Some(m0), Some(m1)) => {
|
||||
// Foreground and background each move on their own vectors.
|
||||
let (fa, fb) = (times_mask(&frames[i0], m0, false), times_mask(&frames[i1], m1, false));
|
||||
let (ga, gb) = (times_mask(&frames[i0], m0, true), times_mask(&frames[i1], m1, true));
|
||||
let mut own = |a: &Image, b: &Image| {
|
||||
if warps.is_some() {
|
||||
return flow;
|
||||
}
|
||||
flows.push((None, timewarp_flow(a, b, &pm)));
|
||||
flows.len() - 1
|
||||
};
|
||||
let (fg_flow, bg_flow) = (own(&fa, &fb), own(&ga, &gb));
|
||||
let fg = [add(&mut frames, fa), add(&mut frames, fb), fg_flow];
|
||||
let bg = [add(&mut frames, ga), add(&mut frames, gb), bg_flow];
|
||||
TwStep::Layered { fg, bg, w: frac, show }
|
||||
}
|
||||
_ => TwStep::Motion { a: i0, b: i1, w: frac, flow },
|
||||
}
|
||||
};
|
||||
steps.push((step, wgt));
|
||||
}
|
||||
Some(TimewarpPlan { grid: fr.grid, frames: fr.imgs, steps, flows: flows.into_iter().map(|(_, f)| f).collect(), pm })
|
||||
Some(TimewarpPlan { grid: fr.grid, frames, steps, flows: flows.into_iter().map(|(_, f)| f).collect(), pm })
|
||||
}
|
||||
|
||||
/// Timewarp. Whole Frames shows the nearest source frame, Frame Mix cross-fades the two
|
||||
|
|
@ -809,108 +872,30 @@ pub fn timewarp_plan(ctx: &EffectCtx) -> Option<TimewarpPlan> {
|
|||
/// Warp Layer supplies the motion instead of the layer itself. Motion Blur averages Shutter
|
||||
/// Samples instants across the shutter.
|
||||
fn timewarp(ctx: &EffectCtx, b: Buf) -> Buf {
|
||||
if ctx.env.host.is_none() {
|
||||
return b;
|
||||
match timewarp_plan(ctx) {
|
||||
Some(plan) => plan.render_cpu(),
|
||||
None => b,
|
||||
}
|
||||
if ctx.params.get("matteLayer").and_then(Value::as_layer).is_none() {
|
||||
return match timewarp_plan(ctx) {
|
||||
Some(plan) => plan.render_cpu(),
|
||||
None => b,
|
||||
};
|
||||
}
|
||||
// Instants of source time (with weights) across the shutter; each needs the frame at or
|
||||
// before it and the next one.
|
||||
let (instants, times, fps) = timewarp_instants(ctx);
|
||||
let split = |s: f64| tw_split(s, fps);
|
||||
let method = ctx.params.e("method");
|
||||
let one = method > 0 && ctx.params.b("tuning/buildFromOneImage");
|
||||
let Some(fr) = fetch(ctx, ×) else { return b };
|
||||
let grid = fr.grid.clone();
|
||||
let frame = |t: f64| -> &Image {
|
||||
let k = times.iter().position(|q| (q - t).abs() < 1e-9).unwrap_or(0);
|
||||
&fr.imgs[k]
|
||||
};
|
||||
let pm = PixelMotion::from(ctx, grid.scale);
|
||||
let mattes = fetch_layer(ctx, "matteLayer", ×, &grid);
|
||||
let warps = fetch_layer(ctx, "warpLayer", ×, &grid);
|
||||
let matte_ch = ctx.params.e("matteChannel");
|
||||
let show = ctx.params.e("show");
|
||||
let mut flows: Vec<((i64, bool), Flow)> = vec![];
|
||||
let mut out_frames = vec![];
|
||||
for &(s, wgt) in &instants {
|
||||
let (f0, frac) = split(s);
|
||||
let (t0, t1) = (f0 / fps, (f0 + 1.0) / fps);
|
||||
let (a, bb) = (frame(t0), frame(t1));
|
||||
let idx = |t: f64| times.iter().position(|q| (q - t).abs() < 1e-9).unwrap_or(0);
|
||||
let ma = mattes.as_ref().map(|m| matte_of(&m[idx(t0)], matte_ch));
|
||||
let mb = mattes.as_ref().map(|m| matte_of(&m[idx(t1)], matte_ch));
|
||||
let img = if method == 0 || frac < 1e-4 || (one && method == 1) {
|
||||
// Whole frames (Build From One Image with Frame Mix: the nearest frame).
|
||||
let pick = if method == 0 || frac < 1e-4 { 0.0 } else { frac.round() };
|
||||
let base = if pick < 0.5 { a } else { bb };
|
||||
match (show, &ma, &mb) {
|
||||
(1, Some(m), _) if pick < 0.5 => times_mask(base, m, false),
|
||||
(2, Some(m), _) if pick < 0.5 => times_mask(base, m, true),
|
||||
(1, _, Some(m)) => times_mask(base, m, false),
|
||||
(2, _, Some(m)) => times_mask(base, m, true),
|
||||
(3, Some(m), _) => matte_image(if pick < 0.5 { m } else { mb.as_ref().unwrap_or(m) }, base),
|
||||
_ => base.clone(),
|
||||
}
|
||||
} else if method == 1 {
|
||||
match (&ma, &mb, show) {
|
||||
(Some(m0), Some(m1), 3) => effectcraft_raster::flow::mix(&matte_image(m0, a), &matte_image(m1, bb), frac),
|
||||
(Some(m0), Some(m1), 1 | 2) => effectcraft_raster::flow::mix(×_mask(a, m0, show == 2), ×_mask(bb, m1, show == 2), frac),
|
||||
_ => effectcraft_raster::flow::mix(a, bb, frac),
|
||||
}
|
||||
} else {
|
||||
// Pixel Motion: vectors from the warp layer if any, else from the layer.
|
||||
let key = ((f0 as i64), false);
|
||||
let flow_ab = |fa: &Image, fb: &Image| -> Flow { timewarp_flow(fa, fb, &pm) };
|
||||
let flow = match flows.iter().find(|(k, _)| *k == key) {
|
||||
Some((_, f)) => f.clone(),
|
||||
None => {
|
||||
let f = match &warps {
|
||||
Some(w) => flow_ab(&w[idx(t0)], &w[idx(t1)]),
|
||||
None => flow_ab(a, bb),
|
||||
};
|
||||
flows.push((key, f.clone()));
|
||||
f
|
||||
}
|
||||
|
||||
/// Pixel Motion with a Matte Layer: the foreground and background frames built on their own
|
||||
/// vectors, shown as Show asks (0 Normal: foreground over background).
|
||||
fn layered(fg: Image, bg: Image, show: u32) -> Image {
|
||||
match show {
|
||||
1 => fg,
|
||||
2 => bg,
|
||||
3 => matte_image(&fg.data.iter().map(|p| p[3]).collect::<Vec<_>>(), &fg),
|
||||
_ => {
|
||||
let mut o = bg;
|
||||
o.data.par_iter_mut().zip(fg.data.par_iter()).for_each(|(p, q)| {
|
||||
let k = 1.0 - q[3];
|
||||
for c in 0..4 {
|
||||
p[c] = q[c] + p[c] * k;
|
||||
}
|
||||
};
|
||||
match (&ma, &mb) {
|
||||
(Some(m0), Some(m1)) => {
|
||||
// Foreground and background each move on their own vectors.
|
||||
let (fa, fb) = (times_mask(a, m0, false), times_mask(bb, m1, false));
|
||||
let (ga, gb) = (times_mask(a, m0, true), times_mask(bb, m1, true));
|
||||
let fg_flow = if warps.is_some() { flow.clone() } else { flow_ab(&fa, &fb) };
|
||||
let bg_flow = if warps.is_some() { flow.clone() } else { flow_ab(&ga, &gb) };
|
||||
let fg = timewarp_interpolate(&fa, &fb, frac, &fg_flow, &pm);
|
||||
let bg = timewarp_interpolate(&ga, &gb, frac, &bg_flow, &pm);
|
||||
match show {
|
||||
1 => fg,
|
||||
2 => bg,
|
||||
3 => matte_image(&fg.data.iter().map(|p| p[3]).collect::<Vec<_>>(), &fg),
|
||||
_ => {
|
||||
let mut o = bg;
|
||||
o.data.par_iter_mut().zip(fg.data.par_iter()).for_each(|(p, q)| {
|
||||
let k = 1.0 - q[3];
|
||||
for c in 0..4 {
|
||||
p[c] = q[c] + p[c] * k;
|
||||
}
|
||||
});
|
||||
o
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => timewarp_interpolate(a, bb, frac, &flow, &pm),
|
||||
}
|
||||
};
|
||||
out_frames.push((img, wgt));
|
||||
});
|
||||
o
|
||||
}
|
||||
}
|
||||
let imgs: Vec<Image> = out_frames.iter().map(|(i, _)| i.clone()).collect();
|
||||
let weights: Vec<f32> = out_frames.iter().map(|(_, w)| *w).collect();
|
||||
let img = if imgs.len() == 1 { imgs.into_iter().next().unwrap_or_default() } else { average(&imgs, &weights) };
|
||||
with_img(grid, img)
|
||||
}
|
||||
|
||||
/// A matte shown as opaque grey.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,239 @@
|
|||
//! Golden hashes of the CPU Timewarp (M13.29), with and without a Matte Layer.
|
||||
//!
|
||||
//! Timewarp's frame building is shared with the GPU compositor as a plan
|
||||
//! (`effectcraft_effects::timewarp_plan`); this pins the CPU output of every method / Show /
|
||||
//! Matte Channel combination, with motion blur and a Warp Layer, so refactoring the plan keeps
|
||||
//! it bit-identical. Same hashing and re-pinning (`SIM_GOLDEN_PRINT=1`) as `sim_golden.rs`;
|
||||
//! pinned for aarch64 macOS.
|
||||
|
||||
use effectcraft_effects::{Buf, EffectCtx, EffectEnv, EffectHost, LayerPixels, Params, apply, default_value, find};
|
||||
use effectcraft_keyframe::Value;
|
||||
use effectcraft_raster::Image;
|
||||
|
||||
const W: u32 = 48;
|
||||
const H: u32 = 32;
|
||||
const FPS: f64 = 10.0;
|
||||
/// Layer ids served by [`Host`]: the same texture standing still (a Warp Layer), a matte white
|
||||
/// on its left part with a soft edge, and a luminance matte.
|
||||
const STILL: u64 = 7;
|
||||
const MATTE: u64 = 8;
|
||||
const LUMA: u64 = 9;
|
||||
|
||||
/// A texture shifted right by `dx` px (premultiplied, partly transparent).
|
||||
fn texture(dx: f64) -> Image {
|
||||
let mut img = Image::new(W, H);
|
||||
for y in 0..H {
|
||||
for x in 0..W {
|
||||
let u = x as f64 - dx;
|
||||
let a = if (10..14).contains(&y) { 0.5 } else { 1.0 };
|
||||
let c = [(0.5 + 0.4 * (u * 0.4).sin()) as f32, (0.5 + 0.4 * (y as f64 * 0.3 + u * 0.1).cos()) as f32, ((u * 0.05).rem_euclid(1.0)) as f32];
|
||||
img.set(x, y, [c[0] * a, c[1] * a, c[2] * a, a]);
|
||||
}
|
||||
}
|
||||
img
|
||||
}
|
||||
|
||||
struct Host;
|
||||
impl EffectHost for Host {
|
||||
fn layer(&self, _: u64, _: bool) -> Option<LayerPixels> {
|
||||
None
|
||||
}
|
||||
fn audio(&self, _: u64, _: f64, _: usize, _: u32) -> Option<Vec<f32>> {
|
||||
None
|
||||
}
|
||||
fn self_at(&self, t: f64, _: usize) -> Option<Buf> {
|
||||
Some(Buf { img: texture((t * FPS).round() * 2.0), offset: [0.0; 2], scale: 1.0 })
|
||||
}
|
||||
fn layer_at(&self, id: u64, t: f64, _: bool) -> Option<LayerPixels> {
|
||||
let img = match id {
|
||||
STILL => texture(0.0),
|
||||
LUMA => {
|
||||
let mut m = Image::new(W, H);
|
||||
for y in 0..H {
|
||||
for x in 0..W {
|
||||
let l = ((x + y) as f32 / (W + H) as f32 + (t * 0.3) as f32).fract();
|
||||
m.set(x, y, [l, l, l, 1.0]);
|
||||
}
|
||||
}
|
||||
m
|
||||
}
|
||||
_ => {
|
||||
let mut m = Image::new(W, H);
|
||||
let edge = 20.0 + (t * FPS).round() * 1.5;
|
||||
for y in 0..H {
|
||||
for x in 0..W {
|
||||
let a = ((edge - x as f64) / 3.0).clamp(0.0, 1.0) as f32;
|
||||
m.set(x, y, [a, a, a, a]);
|
||||
}
|
||||
}
|
||||
m
|
||||
}
|
||||
};
|
||||
Some(LayerPixels { buf: Buf { img, offset: [0.0; 2], scale: 1.0 }, size: [W as f64, H as f64] })
|
||||
}
|
||||
}
|
||||
|
||||
fn n(v: f64) -> Value {
|
||||
Value::Scalar(v)
|
||||
}
|
||||
fn e(v: u32) -> Value {
|
||||
Value::Enum(v)
|
||||
}
|
||||
fn l(id: u64) -> Value {
|
||||
Value::Layer(Some(id))
|
||||
}
|
||||
|
||||
type Set = Vec<(&'static str, Value)>;
|
||||
|
||||
fn cases() -> Vec<Set> {
|
||||
let mut v: Vec<Set> = vec![vec![], vec![("method", e(0))], vec![("method", e(1))]];
|
||||
for method in 0..3 {
|
||||
for show in 0..4 {
|
||||
v.push(vec![("method", e(method)), ("matteLayer", l(MATTE)), ("show", e(show)), ("speed", n(37.0))]);
|
||||
}
|
||||
}
|
||||
for ch in 1..4 {
|
||||
v.push(vec![("matteLayer", l(LUMA)), ("matteChannel", e(ch)), ("speed", n(63.0))]);
|
||||
}
|
||||
v.push(vec![("matteLayer", l(MATTE)), ("warpLayer", l(STILL)), ("speed", n(41.0))]);
|
||||
v.push(vec![("matteLayer", l(MATTE)), ("motionBlur/enableMotionBlur", Value::Bool(true)), ("motionBlur/shutterSamples", n(3.0)), ("show", e(1))]);
|
||||
v.push(vec![("matteLayer", l(MATTE)), ("method", e(1)), ("tuning/buildFromOneImage", Value::Bool(true)), ("show", e(2))]);
|
||||
v.push(vec![("matteLayer", l(MATTE)), ("tuning/buildFromOneImage", Value::Bool(true)), ("speed", n(28.0))]);
|
||||
v.push(vec![("matteLayer", l(MATTE)), ("adjustTimeBy", e(1)), ("sourceFrame", n(4.4)), ("show", e(3))]);
|
||||
v
|
||||
}
|
||||
|
||||
const TIMES: [f64; 3] = [0.55, 1.23, 0.8];
|
||||
|
||||
fn fnv(img: &Image) -> u64 {
|
||||
let mut h = 0xcbf2_9ce4_8422_2325u64;
|
||||
for b in [img.width, img.height].iter().flat_map(|v| v.to_le_bytes()) {
|
||||
h = (h ^ b as u64).wrapping_mul(0x100_0000_01b3);
|
||||
}
|
||||
for p in &img.data {
|
||||
for c in p {
|
||||
for b in c.to_bits().to_le_bytes() {
|
||||
h = (h ^ b as u64).wrapping_mul(0x100_0000_01b3);
|
||||
}
|
||||
}
|
||||
}
|
||||
h
|
||||
}
|
||||
|
||||
fn render(set: &Set, time: f64) -> Image {
|
||||
let spec = find("ec.time.timewarp").unwrap();
|
||||
let size = [W as f64, H as f64];
|
||||
let mut params = Params { values: spec.params.iter().map(|p| (p.id.to_string(), default_value(p, size))).collect() };
|
||||
for (k, v) in set {
|
||||
assert!(params.values.contains_key(*k), "unknown param {k}");
|
||||
params.values.insert(k.to_string(), v.clone());
|
||||
}
|
||||
let host = Host;
|
||||
let env = EffectEnv { host: Some(&host), frame_rate: FPS, comp_time: time, ..Default::default() };
|
||||
let ctx = EffectCtx { params: ¶ms, time, layer_size: size, seed: 1, adjustment: false, env };
|
||||
apply(spec, &ctx, Buf { img: texture(0.0), offset: [0.0; 2], scale: 1.0 }).img
|
||||
}
|
||||
|
||||
fn hashes() -> Vec<(String, u64)> {
|
||||
let mut v = Vec::new();
|
||||
for (si, set) in cases().iter().enumerate() {
|
||||
for t in TIMES {
|
||||
v.push((format!("timewarp/{si}/t{t}"), fnv(&render(set, t))));
|
||||
}
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn timewarp_golden_hashes() {
|
||||
let got = hashes();
|
||||
if std::env::var_os("SIM_GOLDEN_PRINT").is_some() {
|
||||
for (name, h) in &got {
|
||||
println!(" (\"{name}\", 0x{h:016x}),");
|
||||
}
|
||||
}
|
||||
assert_eq!(got, hashes(), "Timewarp renders deterministically");
|
||||
if cfg!(all(target_os = "macos", target_arch = "aarch64")) {
|
||||
let bad: Vec<String> = got
|
||||
.iter()
|
||||
.zip(GOLDEN)
|
||||
.filter(|((n, h), (gn, gh))| n != gn || h != gh)
|
||||
.map(|((n, h), (_, gh))| format!("{n}: 0x{h:016x} (pinned 0x{gh:016x})"))
|
||||
.collect();
|
||||
assert_eq!(got.len(), GOLDEN.len(), "case count");
|
||||
assert!(bad.is_empty(), "CPU output changed:\n{}", bad.join("\n"));
|
||||
}
|
||||
}
|
||||
|
||||
/// Pinned hashes (aarch64 macOS).
|
||||
const GOLDEN: &[(&str, u64)] = &[
|
||||
("timewarp/0/t0.55", 0x85905ea4083d5052),
|
||||
("timewarp/0/t1.23", 0x6499c6f962fed8e7),
|
||||
("timewarp/0/t0.8", 0x48493cd23ed32a5f),
|
||||
("timewarp/1/t0.55", 0x9eca0f14f7cb2d95),
|
||||
("timewarp/1/t1.23", 0xd0c16d4df51315b4),
|
||||
("timewarp/1/t0.8", 0x48493cd23ed32a5f),
|
||||
("timewarp/2/t0.55", 0x9d3703747d8e5b7f),
|
||||
("timewarp/2/t1.23", 0xa9ea3d660b9fd38b),
|
||||
("timewarp/2/t0.8", 0x48493cd23ed32a5f),
|
||||
("timewarp/3/t0.55", 0x9eca0f14f7cb2d95),
|
||||
("timewarp/3/t1.23", 0x48493cd23ed32a5f),
|
||||
("timewarp/3/t0.8", 0x9eca0f14f7cb2d95),
|
||||
("timewarp/4/t0.55", 0x91cbdc02063b9105),
|
||||
("timewarp/4/t1.23", 0x6060a22b299bc92d),
|
||||
("timewarp/4/t0.8", 0x91cbdc02063b9105),
|
||||
("timewarp/5/t0.55", 0xc2e2170269f47324),
|
||||
("timewarp/5/t1.23", 0x3cea3a307e21500d),
|
||||
("timewarp/5/t0.8", 0xc2e2170269f47324),
|
||||
("timewarp/6/t0.55", 0xe55ce2acace7ba15),
|
||||
("timewarp/6/t1.23", 0x6a90b9e251de6d15),
|
||||
("timewarp/6/t0.8", 0xe55ce2acace7ba15),
|
||||
("timewarp/7/t0.55", 0x54020055b7d248c6),
|
||||
("timewarp/7/t1.23", 0x48d89e15e1f7801c),
|
||||
("timewarp/7/t0.8", 0x628bc4aba205a8a8),
|
||||
("timewarp/8/t0.55", 0x467f6294e2c06d6c),
|
||||
("timewarp/8/t1.23", 0xfd442392677e46ad),
|
||||
("timewarp/8/t0.8", 0xc26b69bf0e79c9c3),
|
||||
("timewarp/9/t0.55", 0xd2fddb3350b55277),
|
||||
("timewarp/9/t1.23", 0x16807a7efa8f989a),
|
||||
("timewarp/9/t0.8", 0xb6a045bcaccdc2a2),
|
||||
("timewarp/10/t0.55", 0xa86ae0542d872015),
|
||||
("timewarp/10/t1.23", 0x86dba3262d03f3d5),
|
||||
("timewarp/10/t0.8", 0xfc37965f9c2e51d5),
|
||||
("timewarp/11/t0.55", 0x240ee936695325a8),
|
||||
("timewarp/11/t1.23", 0x7fe2ff24fedcd3f1),
|
||||
("timewarp/11/t0.8", 0xd57ed4c5bfca415e),
|
||||
("timewarp/12/t0.55", 0xad462efd17ecf4be),
|
||||
("timewarp/12/t1.23", 0xd05b6e67e92eaf90),
|
||||
("timewarp/12/t0.8", 0xcc3f297acfa6b646),
|
||||
("timewarp/13/t0.55", 0x80821631b420f5fc),
|
||||
("timewarp/13/t1.23", 0x69ef153147214217),
|
||||
("timewarp/13/t0.8", 0xe81297c3aa0752fe),
|
||||
("timewarp/14/t0.55", 0x79ff11a84d24d2f4),
|
||||
("timewarp/14/t1.23", 0x54fccecd7a87efb5),
|
||||
("timewarp/14/t0.8", 0x0055f9cce950e80f),
|
||||
("timewarp/15/t0.55", 0x193233cd7098d460),
|
||||
("timewarp/15/t1.23", 0x0171866bfbe232b3),
|
||||
("timewarp/15/t0.8", 0x5a15d4a6e85931d7),
|
||||
("timewarp/16/t0.55", 0x0d4df26b38ca39a0),
|
||||
("timewarp/16/t1.23", 0x7502f65ccf41ce0e),
|
||||
("timewarp/16/t0.8", 0x39b6e04e7935b0fe),
|
||||
("timewarp/17/t0.55", 0x99ae36e79014f5dc),
|
||||
("timewarp/17/t1.23", 0xa844571a39e97251),
|
||||
("timewarp/17/t0.8", 0x2e6961e16f50eec6),
|
||||
("timewarp/18/t0.55", 0x9d6d36c8d9303a6e),
|
||||
("timewarp/18/t1.23", 0x63794786e758ad00),
|
||||
("timewarp/18/t0.8", 0x2bc9775fc6f5723e),
|
||||
("timewarp/19/t0.55", 0x11c1925988c75ab0),
|
||||
("timewarp/19/t1.23", 0x19c13bccca289b25),
|
||||
("timewarp/19/t0.8", 0x7f5ec6ec4c3a2aeb),
|
||||
("timewarp/20/t0.55", 0x386ca6db13d4675b),
|
||||
("timewarp/20/t1.23", 0xaee266701f5f9936),
|
||||
("timewarp/20/t0.8", 0x3cea3a307e21500d),
|
||||
("timewarp/21/t0.55", 0x21f0abd02bad8359),
|
||||
("timewarp/21/t1.23", 0xe8c75365a1d6505a),
|
||||
("timewarp/21/t0.8", 0x3ee1c9c17c7760a1),
|
||||
("timewarp/22/t0.55", 0xdc518b5e0fc85047),
|
||||
("timewarp/22/t1.23", 0xdc518b5e0fc85047),
|
||||
("timewarp/22/t0.8", 0xdc518b5e0fc85047),
|
||||
];
|
||||
|
|
@ -319,6 +319,7 @@ impl GpuContext {
|
|||
include_str!("shaders/fx_depth.wgsl"),
|
||||
include_str!("shaders/fx_lut.wgsl"),
|
||||
include_str!("shaders/fx_sim.wgsl"),
|
||||
include_str!("shaders/fx_particles.wgsl"),
|
||||
include_str!("shaders/fx_vr.wgsl"),
|
||||
include_str!("shaders/fx_light.wgsl"),
|
||||
include_str!("shaders/fx_transition.wgsl"),
|
||||
|
|
@ -387,6 +388,7 @@ impl GpuContext {
|
|||
.chain(crate::fx_depth::KERNELS)
|
||||
.chain(crate::fx_lut::KERNELS)
|
||||
.chain(crate::fx_sim::KERNELS)
|
||||
.chain(crate::fx_particles::KERNELS)
|
||||
.chain(crate::fx_vr::KERNELS)
|
||||
.chain(crate::fx_light::KERNELS)
|
||||
.chain(crate::fx_transition::KERNELS)
|
||||
|
|
|
|||
|
|
@ -170,6 +170,7 @@ fn apply(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
|||
_ if crate::fx_depth::IDS.contains(&id) => crate::fx_depth::apply(e, id, ctx, b),
|
||||
_ if crate::fx_lut::IDS.contains(&id) => crate::fx_lut::apply(e, id, ctx, b),
|
||||
_ if crate::fx_sim::IDS.contains(&id) => crate::fx_sim::apply(e, id, ctx, b),
|
||||
_ if crate::fx_particles::IDS.contains(&id) => crate::fx_particles::apply(e, id, ctx, b),
|
||||
_ if crate::fx_vr::IDS.contains(&id) => crate::fx_vr::apply(e, id, ctx, b),
|
||||
_ if crate::fx_light::IDS.contains(&id) => crate::fx_light::apply(e, id, ctx, b),
|
||||
_ if crate::fx_transition::IDS.contains(&id) => crate::fx_transition::apply(e, id, ctx, b),
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@
|
|||
//! iterative 3D inverse): hardware 3D-texture filtering would round the interpolation weights
|
||||
//! and cannot do tetrahedral interpolation. The CPU evaluates transfer functions, CDLs and
|
||||
//! Color Profile Converter's matrix in f64; the kernel in f32 (within the 1e-3 tolerance).
|
||||
//! Custom `.ocio` configurations render on the CPU.
|
||||
//! Custom `.ocio` configurations compile their colour spaces' transforms (matrix and offset,
|
||||
//! exponent, log / log-affine, range, CDL, file LUTs, groups, inverses) to the same program.
|
||||
|
||||
use effectcraft_effects::{ColorOp, EffectCtx, Lut, Straight, Tf};
|
||||
|
||||
|
|
@ -105,6 +106,23 @@ fn encode(ops: &[ColorOp]) -> Option<Vec<f32>> {
|
|||
ColorOp::Tonemap { inverse } => prog.extend([5.0, *inverse as u32 as f32]),
|
||||
ColorOp::Gamut(l) => prog.extend([6.0, l[0] as f32, l[1] as f32, l[2] as f32]),
|
||||
ColorOp::Max0 => prog.push(7.0),
|
||||
ColorOp::Affine { m, pre, post } => {
|
||||
prog.push(9.0);
|
||||
for r in m {
|
||||
prog.extend(r);
|
||||
}
|
||||
prog.extend(pre);
|
||||
prog.extend(post);
|
||||
}
|
||||
ColorOp::Pow(e) => {
|
||||
prog.push(10.0);
|
||||
prog.extend(e);
|
||||
}
|
||||
ColorOp::LogAffine { base, log_slope, log_offset, lin_slope, lin_offset, inverse } => {
|
||||
prog.extend([11.0, *base]);
|
||||
prog.extend(log_slope.iter().chain(log_offset).chain(lin_slope).chain(lin_offset));
|
||||
prog.extend([*inverse as u32 as f32, f64::MIN_POSITIVE.ln() as f32]);
|
||||
}
|
||||
ColorOp::Lut { lut, shaper, interp, inverse } => {
|
||||
prog.extend([8.0, 0.0, *interp as f32, *inverse as u32 as f32, -1.0]);
|
||||
let mut t = vec![];
|
||||
|
|
|
|||
|
|
@ -0,0 +1,284 @@
|
|||
//! GPU effects, particle render passes (kernels in `shaders/fx_particles.wgsl`, every entry
|
||||
//! point prefixed `fxp_`): CC Particle World, CC Particle Systems II, Particle Playground, CC
|
||||
//! Ball Action, CC Pixel Polly, CC Scatterize and Curl Noise; plus Foam's textured discs,
|
||||
//! reflections and flow-map preview (`fx_sim` draws Foam's bubble sprites).
|
||||
//!
|
||||
//! As in `fx_sim`, each frame's plan is built on the CPU and shared with the CPU effect, which
|
||||
//! stays the oracle: the particle systems' sprites (`effects::sprite_plan`, from the stepped
|
||||
//! simulation — on the GPU's `ParticleSim` when the host offers it — sorted as the CPU sorts
|
||||
//! them), Particle Playground's dots, text strokes and Layer Map frames
|
||||
//! (`effects::playground_plan`), and the plans of the effects that read the layer's pixels to
|
||||
//! place or colour their particles (`effects::pixel_plan`: CC Ball Action's balls, CC
|
||||
//! Scatterize's pixel dots, CC Pixel Polly's shards; the frame is read back once). Rasterising
|
||||
//! runs here: items are tiled like `fx_sim`'s and `fxp_raster` composites each pixel's items in
|
||||
//! the CPU's order over a base image, so a plan too large for one item table is drawn in
|
||||
//! several passes, each over the previous one ([`Chunked`]; the per-pixel arithmetic is the
|
||||
//! same). Particle Playground's Layer Map frames are packed into one atlas texture.
|
||||
//!
|
||||
//! Curl Noise runs fully here: the fBm potential, its curl (central differences, edges
|
||||
//! clamped) and the backwards streamline trace, each a kernel mirroring the CPU's passes.
|
||||
|
||||
use effectcraft_effects::{Buf, EffectCtx, PixelPlan};
|
||||
use effectcraft_raster::Image;
|
||||
|
||||
use crate::context::{Enc, GpuImage, Params};
|
||||
use crate::effects::GBuf;
|
||||
use crate::fx_sim::{Items, piece_pass, sprite_layer, sprites};
|
||||
|
||||
/// Compute entry points in `fx_particles.wgsl`.
|
||||
pub(crate) const KERNELS: &[&str] = &["fxp_raster", "fxp_under", "fxp_curl_pot", "fxp_curl_vel", "fxp_curl_view", "fxp_curl_flow"];
|
||||
|
||||
/// Effect ids implemented here.
|
||||
pub(crate) const IDS: &[&str] = &[
|
||||
"ec.sim.ccparticleworld",
|
||||
"ec.sim.ccparticlesystems2",
|
||||
"ec.sim.particleplayground",
|
||||
"ec.sim.ccballaction",
|
||||
"ec.sim.ccpixelpolly",
|
||||
"ec.sim.ccscatterize",
|
||||
"ec.noise.curlnoise",
|
||||
];
|
||||
|
||||
/// Run effect `id` (one of [`IDS`]); `None` = this parameter combination runs on the CPU.
|
||||
pub(crate) fn apply(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let geo = Buf { img: Image::new(0, 0), offset: b.offset, scale: b.scale };
|
||||
match id {
|
||||
"ec.sim.ccparticleworld" | "ec.sim.ccparticlesystems2" => match effectcraft_effects::sprite_plan(id, ctx, &geo) {
|
||||
Some(plan) => sprites(e, b, &plan),
|
||||
None => Some(b),
|
||||
},
|
||||
"ec.sim.particleplayground" => playground(e, ctx, b, &geo),
|
||||
"ec.noise.curlnoise" => curl_noise(e, ctx, b),
|
||||
_ => {
|
||||
// The plan reads the layer's pixels.
|
||||
let img = e.download(&b.img)?;
|
||||
let cpu = Buf { img, offset: b.offset, scale: b.scale };
|
||||
match effectcraft_effects::pixel_plan(id, ctx, &cpu) {
|
||||
Some(PixelPlan::Sprites(plan)) => sprites(e, b, &plan),
|
||||
Some(PixelPlan::Pieces(plan)) => piece_pass(e, b, &plan),
|
||||
None => Some(b),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- chunked raster
|
||||
|
||||
/// Items rasterised by `fxp_raster` over a base image (transparent at first), flushed into a
|
||||
/// new pass whenever the item table would outgrow one kernel's data. `kind`: 0 sprites, 1
|
||||
/// Layer Map frames, 2 Foam texture discs, 3 Foam reflection discs, 4 pieces without a back
|
||||
/// texture; `mode`: 0 over, 1 add, 2 Foam's disc compositing.
|
||||
pub(crate) struct Chunked {
|
||||
kind: u32,
|
||||
mode: u32,
|
||||
stride: usize,
|
||||
w: u32,
|
||||
h: u32,
|
||||
items: Items,
|
||||
base: Option<GpuImage>,
|
||||
ok: bool,
|
||||
/// Kernel parameters besides `u[0]` and `u[1].x`.
|
||||
pub p: Params,
|
||||
}
|
||||
|
||||
impl Chunked {
|
||||
/// `stride` = floats per item, its four bounds included.
|
||||
pub(crate) fn new(kind: u32, mode: u32, stride: usize, w: u32, h: u32) -> Chunked {
|
||||
Chunked { kind, mode, stride, w, h, items: Items::new(stride, w, h), base: None, ok: true, p: Params::default() }
|
||||
}
|
||||
|
||||
/// Start from `base` instead of a transparent frame.
|
||||
pub(crate) fn over(mut self, base: GpuImage) -> Chunked {
|
||||
self.base = Some(base);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add an item (`rec` without its bounds) covering float bounds `bb`; `src` is the texture
|
||||
/// the items sample.
|
||||
pub(crate) fn push(&mut self, e: &mut Enc, src: &GpuImage, bb: Option<[f32; 4]>, rec: &[f32]) {
|
||||
if !self.items.fits(bb) && !self.items.is_empty() {
|
||||
self.flush(e, src);
|
||||
}
|
||||
self.items.push(bb, rec);
|
||||
}
|
||||
|
||||
fn flush(&mut self, e: &mut Enc, src: &GpuImage) {
|
||||
let items = std::mem::replace(&mut self.items, Items::new(self.stride, self.w, self.h));
|
||||
let tx = items.tx as u32;
|
||||
let Some((data, items_at, stride)) = items.data() else {
|
||||
self.ok = false;
|
||||
return;
|
||||
};
|
||||
let buf = e.data(&data);
|
||||
let base = self.base.take().unwrap_or_else(|| e.zeros(self.w, self.h));
|
||||
let mut p = self.p;
|
||||
p.u[0] = [self.kind, self.mode, items_at, stride];
|
||||
p.u[1][0] = tx;
|
||||
let out = e.scratch(self.w, self.h);
|
||||
e.pixels("fxp_raster", &p, src, Some(&base), &out, Some(&buf));
|
||||
self.base = Some(out);
|
||||
}
|
||||
|
||||
/// The result (`None` when an item alone outgrew a kernel's data).
|
||||
pub(crate) fn finish(mut self, e: &mut Enc, src: &GpuImage) -> Option<GpuImage> {
|
||||
if !self.items.is_empty() {
|
||||
self.flush(e, src);
|
||||
}
|
||||
if !self.ok {
|
||||
return None;
|
||||
}
|
||||
Some(match self.base {
|
||||
Some(b) => b,
|
||||
None => e.image(self.w, self.h),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Bounds of a disc drawn over `[floor(x − r − 1), ceil(x + r + 1))` (sim2::foam_disc), as
|
||||
/// inclusive float bounds for [`Items`].
|
||||
fn disc_bounds(x: f64, y: f64, r: f64) -> Option<[f32; 4]> {
|
||||
(r > 0.0).then(|| [(x - r - 1.0) as f32, (y - r - 1.0) as f32, ((x + r + 1.0).ceil() - 1.0) as f32, ((y + r + 1.0).ceil() - 1.0) as f32])
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Foam's extras
|
||||
|
||||
/// Foam: the bubble sprites, then the User Defined texture's discs, the Environment Map's
|
||||
/// reflections and the flow-map preview (sim2::FoamPlan::finish).
|
||||
pub(crate) fn foam(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let (w, h) = (b.img.width, b.img.height);
|
||||
// The plan fits other layers to the buffer: it needs the buffer's size (not its pixels).
|
||||
let geo = Buf { img: Image::new(w, h), offset: b.offset, scale: b.scale };
|
||||
let plan = effectcraft_effects::foam_full_plan(ctx, &geo);
|
||||
let mut fx = sprite_layer(e, &b.img, &plan.sprites)?;
|
||||
if let Some((tex, discs)) = &plan.user {
|
||||
let t = e.g.upload_image(&tex.buf.img)?;
|
||||
let mut ch = Chunked::new(2, 2, 16, w, h).over(fx);
|
||||
for d in discs {
|
||||
let (s, c) = d.rot.sin_cos();
|
||||
let rec = [2.0, d.x as f32, d.y as f32, d.r, s, c, d.fade, tex.size[0] as f32, tex.size[1] as f32, tex.buf.scale as f32];
|
||||
let rec = [&rec[..], &[tex.buf.offset[0] as f32, tex.buf.offset[1] as f32]].concat();
|
||||
ch.push(e, &t, disc_bounds(d.x, d.y, d.r as f64), &rec);
|
||||
}
|
||||
fx = ch.finish(e, &t)?;
|
||||
}
|
||||
if let Some(env) = &plan.env {
|
||||
let t = e.g.upload_image(&env.img)?;
|
||||
let mut ch = Chunked::new(3, 2, 12, w, h).over(fx);
|
||||
for d in &env.discs {
|
||||
let rec = [3.0, d.x as f32, d.y as f32, d.r, env.conv as f32, env.strength, w as f32, h as f32];
|
||||
ch.push(e, &t, disc_bounds(d.x, d.y, d.r as f64), &rec);
|
||||
}
|
||||
fx = ch.finish(e, &t)?;
|
||||
}
|
||||
if let Some(fm) = &plan.flow {
|
||||
let f = e.g.upload_image(fm)?;
|
||||
let out = e.scratch(w, h);
|
||||
e.pixels("fxp_under", &Params::default(), &fx, Some(&f), &out, None);
|
||||
fx = out;
|
||||
}
|
||||
Some(GBuf { img: fx, ..b })
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Particle Playground
|
||||
|
||||
fn playground(e: &mut Enc, ctx: &EffectCtx, b: GBuf, geo: &Buf) -> Option<GBuf> {
|
||||
let plan = effectcraft_effects::playground_plan(ctx, geo);
|
||||
let (w, h) = (b.img.width, b.img.height);
|
||||
let mut fx = sprite_layer(e, &b.img, &plan.sprites)?;
|
||||
if !plan.blits.is_empty() {
|
||||
// The Layer Map frames, packed into one atlas on a grid of equal cells.
|
||||
let cw = plan.frames.iter().map(|f| f.buf.img.width).max().unwrap_or(1).max(1);
|
||||
let chh = plan.frames.iter().map(|f| f.buf.img.height).max().unwrap_or(1).max(1);
|
||||
let max = e.g.max_dim;
|
||||
let cols = (max / cw).max(1).min(plan.frames.len() as u32);
|
||||
let rows = (plan.frames.len() as u32).div_ceil(cols);
|
||||
if !e.g.fits(cw * cols, chh * rows) {
|
||||
return None;
|
||||
}
|
||||
let mut atlas = Image::new(cw * cols, chh * rows);
|
||||
let mut at = vec![];
|
||||
for (i, f) in plan.frames.iter().enumerate() {
|
||||
let (ax, ay) = ((i as u32 % cols) * cw, (i as u32 / cols) * chh);
|
||||
let img = &f.buf.img;
|
||||
for y in 0..img.height {
|
||||
let row = &img.data[(y * img.width) as usize..((y + 1) * img.width) as usize];
|
||||
let o = ((ay + y) * atlas.width + ax) as usize;
|
||||
atlas.data[o..o + row.len()].copy_from_slice(row);
|
||||
}
|
||||
at.push((ax, ay));
|
||||
}
|
||||
let t = e.g.upload_image(&atlas)?;
|
||||
let mut ch = Chunked::new(1, 0, 24, w, h).over(fx);
|
||||
for q in &plan.blits {
|
||||
let lp = &plan.frames[q.frame];
|
||||
if q.scale[0] == 0.0 || q.scale[1] == 0.0 {
|
||||
continue;
|
||||
}
|
||||
// sim3::blit_layer's pixel range.
|
||||
let (fw, fh) = (lp.size[0] * q.scale[0].abs() * b.scale, lp.size[1] * q.scale[1].abs() * b.scale);
|
||||
let r = (fw * fw + fh * fh).sqrt() * 0.5 + 1.0;
|
||||
let bb = [(q.x - r) as f32, (q.y - r) as f32, ((q.x + r).ceil() - 1.0) as f32, ((q.y + r).ceil() - 1.0) as f32];
|
||||
let (sa, ca) = q.angle.to_radians().sin_cos();
|
||||
let (ax, ay) = at[q.frame];
|
||||
let rec = [
|
||||
1.0,
|
||||
q.x as f32,
|
||||
q.y as f32,
|
||||
sa as f32,
|
||||
ca as f32,
|
||||
q.scale[0] as f32,
|
||||
q.scale[1] as f32,
|
||||
b.scale as f32,
|
||||
q.alpha,
|
||||
lp.size[0] as f32,
|
||||
lp.size[1] as f32,
|
||||
lp.buf.scale as f32,
|
||||
lp.buf.offset[0] as f32,
|
||||
lp.buf.offset[1] as f32,
|
||||
ax as f32,
|
||||
ay as f32,
|
||||
lp.buf.img.width as f32,
|
||||
lp.buf.img.height as f32,
|
||||
];
|
||||
ch.push(e, &t, Some(bb), &rec);
|
||||
}
|
||||
fx = ch.finish(e, &t)?;
|
||||
}
|
||||
// The particles replace the layer's own pixels.
|
||||
Some(GBuf { img: fx, ..b })
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Curl Noise
|
||||
|
||||
/// noise2::curl_noise: the potential, its curl and the streamline trace.
|
||||
fn curl_noise(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let pr = ctx.params;
|
||||
let amount = pr.f("displacementAmount") * b.scale;
|
||||
let view = pr.e("view");
|
||||
if amount.abs() < 1e-6 && view == 0 {
|
||||
return Some(b);
|
||||
}
|
||||
let scale = (pr.f("scale").max(1.0) * b.scale) as f32;
|
||||
let rot = pr.f("rotation").to_radians() as f32;
|
||||
let off = pr.v2("offset");
|
||||
let (ox, oy) = ((off[0] * b.scale + b.offset[0]) as f32, (off[1] * b.scale + b.offset[1]) as f32);
|
||||
let evo = (pr.f("evolution") / 360.0) as f32;
|
||||
let seed = pr.f("randomSeed") as i64 as u32 ^ 0x51_7cc1;
|
||||
let steps = pr.f("steps").clamp(1.0, 16.0) as usize;
|
||||
let (w, h) = (b.img.width, b.img.height);
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [seed, steps as u32, (pr.e("edgeBehavior") == 1) as u32, 0];
|
||||
p.f[0] = [ox, oy, scale, pr.f("complexity") as f32];
|
||||
p.f[1] = [rot.cos(), rot.sin(), evo * 4.0, amount as f32 / steps as f32];
|
||||
let pot = e.scratch(w, h);
|
||||
e.pixels("fxp_curl_pot", &p, &b.img, None, &pot, None);
|
||||
let vel = e.scratch(w, h);
|
||||
e.pixels("fxp_curl_vel", &p, &pot, None, &vel, None);
|
||||
let out = e.scratch(w, h);
|
||||
if view == 1 {
|
||||
e.pixels("fxp_curl_view", &p, &vel, None, &out, None);
|
||||
} else {
|
||||
e.pixels("fxp_curl_flow", &p, &b.img, Some(&vel), &out, None);
|
||||
}
|
||||
Some(GBuf { img: out, ..b })
|
||||
}
|
||||
|
|
@ -12,8 +12,11 @@
|
|||
//! (colours that depend on the layer's own pixels — refracting rain, star-burst and hair-root
|
||||
//! colours, bubble refraction — are sampled per pixel from the layer texture). Piece plans read
|
||||
//! the layer's pixels on the CPU (gradient maps and textures default to the layer), so those
|
||||
//! effects read the frame back once. Settings drawn otherwise (Shatter's wireframe views,
|
||||
//! Foam's User Defined texture, Environment Map and flow-map preview) render on the CPU.
|
||||
//! effects read the frame back once. Shatter's wireframe views draw their lines as sprites
|
||||
//! (`effects::pixel_plan`); Foam's User Defined texture, Environment Map and flow-map preview
|
||||
//! are drawn by `fx_particles` over the bubble sprites (`effects::foam_full_plan`). Sprite
|
||||
//! layers and back-less piece plans too large for one item table are drawn in several passes
|
||||
//! (`fx_particles::Chunked`).
|
||||
|
||||
use effectcraft_effects::{Acc, EffectCtx, PieceTex, Post, Shape, SpritePlan, Tint};
|
||||
use effectcraft_raster::Image;
|
||||
|
|
@ -58,6 +61,7 @@ pub(crate) fn apply(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<G
|
|||
effectcraft_effects::WavePlan::Height { st, depth } => wave_height(e, ctx, b, &st, depth.as_deref()),
|
||||
},
|
||||
"ec.sim.shatter" | "ec.sim.carddance" | "ec.transition.cardwipe" => pieces(e, id, ctx, b),
|
||||
"ec.sim.foam" => crate::fx_particles::foam(e, ctx, b),
|
||||
"ec.sim.cchair" => match effectcraft_effects::sprite_plan(id, ctx, &geo) {
|
||||
Some(plan) => sprites(e, b, &plan),
|
||||
None => Some(b),
|
||||
|
|
@ -72,37 +76,56 @@ pub(crate) fn apply(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<G
|
|||
// ---------------------------------------------------------------- tiled item raster
|
||||
|
||||
/// Items with their integer pixel bounds, binned into tiles (sim::raster's bands, finer).
|
||||
struct Items {
|
||||
pub(crate) struct Items {
|
||||
stride: usize,
|
||||
recs: Vec<f32>,
|
||||
w: i32,
|
||||
h: i32,
|
||||
tiles: Vec<Vec<u32>>,
|
||||
tx: i32,
|
||||
pub(crate) tx: i32,
|
||||
/// Item indices in all tiles.
|
||||
n_idx: usize,
|
||||
}
|
||||
|
||||
impl Items {
|
||||
fn new(stride: usize, w: u32, h: u32) -> Items {
|
||||
pub(crate) fn new(stride: usize, w: u32, h: u32) -> Items {
|
||||
let (w, h) = (w as i32, h as i32);
|
||||
let tx = (w + TILE - 1) / TILE;
|
||||
let ty = (h + TILE - 1) / TILE;
|
||||
Items { stride, recs: vec![], w, h, tiles: vec![vec![]; (tx * ty).max(0) as usize], tx }
|
||||
Items { stride, recs: vec![], w, h, tiles: vec![vec![]; (tx * ty).max(0) as usize], tx, n_idx: 0 }
|
||||
}
|
||||
|
||||
/// Add an item (`rec` = its fields, without the bounds) covering float bounds `bb`
|
||||
/// (`None` or empty = skipped, as sim::raster).
|
||||
fn push(&mut self, bb: Option<[f32; 4]>, rec: &[f32]) {
|
||||
let Some(b) = bb else { return };
|
||||
/// The integer pixel bounds of float bounds `bb` (`None` = skipped, as sim::raster).
|
||||
fn bounds(&self, bb: Option<[f32; 4]>) -> Option<[i32; 4]> {
|
||||
let b = bb?;
|
||||
if !(b[0].is_finite() && b[1].is_finite() && b[2].is_finite() && b[3].is_finite()) {
|
||||
return;
|
||||
return None;
|
||||
}
|
||||
let x0 = b[0].floor().max(0.0) as i32;
|
||||
let y0 = b[1].floor().max(0.0) as i32;
|
||||
let x1 = (b[2].ceil() as i32).min(self.w - 1);
|
||||
let y1 = (b[3].ceil() as i32).min(self.h - 1);
|
||||
if x1 < x0 || y1 < y0 || b[2] < 0.0 || b[3] < 0.0 {
|
||||
return;
|
||||
return None;
|
||||
}
|
||||
Some([x0, y0, x1, y1])
|
||||
}
|
||||
|
||||
pub(crate) fn is_empty(&self) -> bool {
|
||||
self.recs.is_empty()
|
||||
}
|
||||
|
||||
/// Whether adding an item covering `bb` keeps the kernel data within [`MAX_FLOATS`].
|
||||
pub(crate) fn fits(&self, bb: Option<[f32; 4]>) -> bool {
|
||||
let Some([x0, y0, x1, y1]) = self.bounds(bb) else { return true };
|
||||
let tiles = ((x1 / TILE - x0 / TILE + 1) * (y1 / TILE - y0 / TILE + 1)) as usize;
|
||||
self.tiles.len() + 1 + self.n_idx + tiles + self.recs.len() + self.stride <= MAX_FLOATS
|
||||
}
|
||||
|
||||
/// Add an item (`rec` = its fields, without the bounds) covering float bounds `bb`
|
||||
/// (`None` or empty = skipped, as sim::raster).
|
||||
pub(crate) fn push(&mut self, bb: Option<[f32; 4]>, rec: &[f32]) {
|
||||
let Some([x0, y0, x1, y1]) = self.bounds(bb) else { return };
|
||||
let i = (self.recs.len() / self.stride) as u32;
|
||||
let start = self.recs.len();
|
||||
self.recs.extend_from_slice(rec);
|
||||
|
|
@ -111,12 +134,13 @@ impl Items {
|
|||
for ty in y0 / TILE..=y1 / TILE {
|
||||
for tx in x0 / TILE..=x1 / TILE {
|
||||
self.tiles[(ty * self.tx + tx) as usize].push(i);
|
||||
self.n_idx += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Kernel data: tile offsets (tiles + 1), item indices, items. `None` when too large.
|
||||
fn data(self) -> Option<(Vec<f32>, u32, u32)> {
|
||||
pub(crate) fn data(self) -> Option<(Vec<f32>, u32, u32)> {
|
||||
let n_tiles = self.tiles.len();
|
||||
let n_idx: usize = self.tiles.iter().map(Vec::len).sum();
|
||||
let total = n_tiles + 1 + n_idx + self.recs.len();
|
||||
|
|
@ -167,9 +191,32 @@ fn sprite_bbox(x: f32, y: f32, r: f32, c3: f32, shape: Shape) -> Option<[f32; 4]
|
|||
})
|
||||
}
|
||||
|
||||
fn sprites(e: &mut Enc, b: GBuf, plan: &SpritePlan) -> Option<GBuf> {
|
||||
/// Draw a sprite plan and meet the layer `b` (sim::SpritePlan::finish).
|
||||
pub(crate) fn sprites(e: &mut Enc, b: GBuf, plan: &SpritePlan) -> Option<GBuf> {
|
||||
let (w, h) = (b.img.width, b.img.height);
|
||||
let mut items = Items::new(24, w, h);
|
||||
let fx = sprite_layer(e, &b.img, plan)?;
|
||||
let mut p = Params::default();
|
||||
match plan.post {
|
||||
Post::Combine(m) => p.u[0] = [0, m, 0, 0],
|
||||
Post::Replace => return Some(GBuf { img: fx, ..b }),
|
||||
Post::Lerp(t) => {
|
||||
p.u[0][0] = 1;
|
||||
p.f[0][0] = t;
|
||||
}
|
||||
Post::OverBlack => p.u[0][0] = 2,
|
||||
}
|
||||
let out = e.scratch(w, h);
|
||||
e.pixels("fxm_post", &p, &b.img, Some(&fx), &out, None);
|
||||
Some(GBuf { img: out, ..b })
|
||||
}
|
||||
|
||||
/// sim::splat of a sprite plan over a transparent frame of `src`'s size (tints sample `src`),
|
||||
/// in as many passes as the item tables need.
|
||||
pub(crate) fn sprite_layer(e: &mut Enc, src: &GpuImage, plan: &SpritePlan) -> Option<GpuImage> {
|
||||
let (w, h) = (src.width, src.height);
|
||||
// Without tints the record stops after the (zero) tint kind.
|
||||
let stride = if plan.tints.is_empty() { 16 } else { 24 };
|
||||
let mut ch = crate::fx_particles::Chunked::new(0, (plan.acc == Acc::Add) as u32, stride, w, h);
|
||||
for (i, s) in plan.sprites.iter().enumerate() {
|
||||
let (kind, dx, dy) = match s.shape {
|
||||
Shape::Soft => (0.0, 0.0, 0.0),
|
||||
|
|
@ -191,22 +238,9 @@ fn sprites(e: &mut Enc, b: GBuf, plan: &SpritePlan) -> Option<GBuf> {
|
|||
rec[11..20].copy_from_slice(&[3.0, x as f32, y as f32, hair[0], hair[1], hair[2], inherit, shade, spec]);
|
||||
}
|
||||
}
|
||||
items.push(sprite_bbox(s.x, s.y, s.r, s.c[3], s.shape), &rec);
|
||||
ch.push(e, src, sprite_bbox(s.x, s.y, s.r, s.c[3], s.shape), &rec[..stride - 4]);
|
||||
}
|
||||
let fx = raster(e, items, 0, plan.acc == Acc::Add, &b.img, None, Params::default(), w, h)?;
|
||||
let mut p = Params::default();
|
||||
match plan.post {
|
||||
Post::Combine(m) => p.u[0] = [0, m, 0, 0],
|
||||
Post::Replace => p.u[0] = [0, 5, 0, 0],
|
||||
Post::Lerp(t) => {
|
||||
p.u[0][0] = 1;
|
||||
p.f[0][0] = t;
|
||||
}
|
||||
Post::OverBlack => p.u[0][0] = 2,
|
||||
}
|
||||
let out = e.scratch(w, h);
|
||||
e.pixels("fxm_post", &p, &b.img, Some(&fx), &out, None);
|
||||
Some(GBuf { img: out, ..b })
|
||||
ch.finish(e, src)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Bubbles / Mr. Mercury
|
||||
|
|
@ -329,10 +363,22 @@ fn pieces(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
|||
// Planning reads the layer (gradient maps and textures default to it).
|
||||
let img = e.download(&b.img)?;
|
||||
let cpu = effectcraft_effects::Buf { img, offset: b.offset, scale: b.scale };
|
||||
if id == "ec.sim.shatter" {
|
||||
// The rendered pieces or the wireframe views' lines.
|
||||
return match effectcraft_effects::pixel_plan(id, ctx, &cpu)? {
|
||||
effectcraft_effects::PixelPlan::Pieces(plan) => piece_pass(e, b, &plan),
|
||||
effectcraft_effects::PixelPlan::Sprites(plan) => sprites(e, b, &plan),
|
||||
};
|
||||
}
|
||||
let Some(plan) = effectcraft_effects::piece_plan(id, ctx, &cpu) else {
|
||||
// Card Wipe before anything moves: the layer as is; Shatter's wireframes: the CPU.
|
||||
// Card Wipe before anything moves: the layer as is.
|
||||
return (id == "ec.transition.cardwipe").then_some(b);
|
||||
};
|
||||
piece_pass(e, b, &plan)
|
||||
}
|
||||
|
||||
/// Draw a piece plan over a transparent frame of `b`'s size (sim2::PiecePlan::finish).
|
||||
pub(crate) fn piece_pass(e: &mut Enc, b: GBuf, plan: &effectcraft_effects::PiecePlan) -> Option<GBuf> {
|
||||
let (w, h) = (b.img.width, b.img.height);
|
||||
let tex = |t: &PieceTex| -> Option<GpuImage> {
|
||||
match t {
|
||||
|
|
@ -346,6 +392,9 @@ fn pieces(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
|||
None => None,
|
||||
};
|
||||
let mut items = Items::new(48, w, h);
|
||||
// Without a back texture the pieces can be drawn in passes over a base (CC Pixel Polly's
|
||||
// fine grids).
|
||||
let mut ch = back.is_none().then(|| crate::fx_particles::Chunked::new(4, 0, 48, w, h));
|
||||
for q in &plan.pieces {
|
||||
let m = q.inv;
|
||||
let mut rec = vec![11.0];
|
||||
|
|
@ -365,7 +414,13 @@ fn pieces(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
|||
None => rec.extend([0.0; 4]),
|
||||
}
|
||||
rec.extend([q.back as u32 as f32, q.back_mirror as f32]);
|
||||
items.push(Some(q.bbox), &rec);
|
||||
match &mut ch {
|
||||
Some(ch) => ch.push(e, &front, Some(q.bbox), &rec),
|
||||
None => items.push(Some(q.bbox), &rec),
|
||||
}
|
||||
}
|
||||
if let Some(ch) = ch {
|
||||
return Some(GBuf { img: ch.finish(e, &front)?, ..b });
|
||||
}
|
||||
let mut p = Params::default();
|
||||
p.u[1][1] = back.is_some() as u32;
|
||||
|
|
|
|||
|
|
@ -10,8 +10,9 @@
|
|||
//! from its frame. Timewarp's motion vectors (block matching and smoothing,
|
||||
//! `effectcraft_raster::flow`) are estimated on the CPU (`effectcraft_effects::timewarp_plan`);
|
||||
//! the frame building (Whole Frames, Frame Mix, Pixel Motion's warp along the vectors with its
|
||||
//! error fallback, the shutter samples' average) runs on the GPU. Timewarp with a Matte Layer
|
||||
//! renders on the CPU (`catalog::gpu_supported`).
|
||||
//! error fallback, the shutter samples' average) runs on the GPU. With a Matte Layer the plan
|
||||
//! also carries the matte-masked frames (and the foreground's and background's own vectors);
|
||||
//! the GPU builds both layers and shows them as Show asks (`ftm_layer`).
|
||||
|
||||
use effectcraft_effects::util::{fit_layer, unpremul};
|
||||
use effectcraft_effects::{Buf, EffectCtx, TwStep};
|
||||
|
|
@ -21,7 +22,7 @@ use crate::context::{Enc, GpuImage, Params};
|
|||
use crate::effects::GBuf;
|
||||
|
||||
/// Compute entry points in `fx_time.wgsl`.
|
||||
pub(crate) const KERNELS: &[&str] = &["ftm_acc", "ftm_mix", "ftm_difference", "ftm_displace", "ftm_motion"];
|
||||
pub(crate) const KERNELS: &[&str] = &["ftm_acc", "ftm_mix", "ftm_layer", "ftm_difference", "ftm_displace", "ftm_motion"];
|
||||
|
||||
/// Effect ids implemented here.
|
||||
pub(crate) const IDS: &[&str] =
|
||||
|
|
@ -177,11 +178,19 @@ fn timewarp(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
|||
if ctx.env.host.is_none() {
|
||||
return Some(b);
|
||||
}
|
||||
// A Matte Layer splits foreground and background: the CPU's path.
|
||||
if ctx.params.get("matteLayer").and_then(Value::as_layer).is_some() {
|
||||
return None;
|
||||
}
|
||||
let Some(plan) = effectcraft_effects::timewarp_plan(ctx) else { return Some(b) };
|
||||
// Pixel Motion's in-between frame of frames a, b along motion field `flow`.
|
||||
let motion = |e: &mut Enc, a: usize, bi: usize, w: f32, flow: usize| -> Option<GpuImage> {
|
||||
let pa = e.g.upload_image(&effectcraft_effects::timewarp_crop(&plan.frames[a], &plan.pm))?;
|
||||
let pb = e.g.upload_image(&effectcraft_effects::timewarp_crop(&plan.frames[bi], &plan.pm))?;
|
||||
let f = &plan.flows[flow];
|
||||
let v: Vec<f32> = f.v.iter().flat_map(|q| [q[0], q[1]]).collect();
|
||||
let data = e.data(&v);
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [if f.v.is_empty() { 0 } else { f.cols as u32 }, f.rows as u32, plan.pm.extreme as u32, plan.pm.build_from_one as u32];
|
||||
p.f[0] = [f.block as f32, w, plan.pm.error_threshold, 0.0];
|
||||
Some(kernel(e, "ftm_motion", &p, &pa, Some(&pb), Some(&data)))
|
||||
};
|
||||
let mut up: Vec<Option<GpuImage>> = vec![None; plan.frames.len()];
|
||||
let mut frame = |e: &mut Enc, i: usize| -> Option<GpuImage> {
|
||||
if up[i].is_none() {
|
||||
|
|
@ -199,16 +208,13 @@ fn timewarp(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
|||
p.f[0][0] = w;
|
||||
kernel(e, "ftm_mix", &p, &a, Some(&bb), None)
|
||||
}
|
||||
TwStep::Motion { a, b: bi, w, flow } => {
|
||||
let pa = e.g.upload_image(&effectcraft_effects::timewarp_crop(&plan.frames[a], &plan.pm))?;
|
||||
let pb = e.g.upload_image(&effectcraft_effects::timewarp_crop(&plan.frames[bi], &plan.pm))?;
|
||||
let f = &plan.flows[flow];
|
||||
let v: Vec<f32> = f.v.iter().flat_map(|q| [q[0], q[1]]).collect();
|
||||
let data = e.data(&v);
|
||||
TwStep::Motion { a, b: bi, w, flow } => motion(e, a, bi, w, flow)?,
|
||||
TwStep::Layered { fg, bg, w, show } => {
|
||||
let f = motion(e, fg[0], fg[1], w, fg[2])?;
|
||||
let g = motion(e, bg[0], bg[1], w, bg[2])?;
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [if f.v.is_empty() { 0 } else { f.cols as u32 }, f.rows as u32, plan.pm.extreme as u32, plan.pm.build_from_one as u32];
|
||||
p.f[0] = [f.block as f32, w, plan.pm.error_threshold, 0.0];
|
||||
kernel(e, "ftm_motion", &p, &pa, Some(&pb), Some(&data))
|
||||
p.u[0][0] = show;
|
||||
kernel(e, "ftm_layer", &p, &f, Some(&g), None)
|
||||
}
|
||||
};
|
||||
imgs.push(img);
|
||||
|
|
|
|||
|
|
@ -6,8 +6,10 @@
|
|||
//! Smear's moved outline, Reshape's outline correspondence, Bend It's arc geometry) and goes to
|
||||
//! the kernels as parameters or tables; the kernels do the per-pixel work in f32. Warp inverts
|
||||
//! its forward map per pixel with the CPU's Newton iterations, relative to the warp centre so
|
||||
//! f32 keeps sub-pixel precision; Bend It's arc distance is evaluated in a cancellation-free
|
||||
//! form so large radii (small bends) keep their precision too.
|
||||
//! f32 keeps sub-pixel precision (strong Fisheye / Twist bends, where the iterations are chaotic
|
||||
//! near the fold, sample the CPU's f64 inverse map, `effects::warp_inverse_map`); Bend It's arc
|
||||
//! distance is evaluated in a cancellation-free form so large radii (small bends) keep their
|
||||
//! precision too.
|
||||
|
||||
use effectcraft_effects::{Buf, EffectCtx, Image};
|
||||
|
||||
|
|
@ -17,6 +19,7 @@ use crate::effects::{GBuf, gaussian_blur};
|
|||
/// Compute entry points in `fx_warp.wgsl`.
|
||||
pub(crate) const KERNELS: &[&str] = &[
|
||||
"fxw_warp",
|
||||
"fxw_remap",
|
||||
"fxw_bendit",
|
||||
"fxw_pageturn",
|
||||
"fxw_smear",
|
||||
|
|
@ -93,6 +96,16 @@ fn warp(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
|
|||
let pad = ((k.abs() + hd.abs() + vd.abs()) * 0.6 * hx.max(hy)).ceil().min(4096.0);
|
||||
b.pad(e, pad as u32 + 1)?;
|
||||
}
|
||||
// Fisheye / Twist (a crease at the unit circle) bent past 50 % or with Horizontal /
|
||||
// Vertical Distortion: Newton's inverse wanders chaotically before converging near the
|
||||
// fold, so f32 would land on other pixels than the CPU's f64. The inverse map is solved on
|
||||
// the CPU (the CPU effect's own solver) and sampled here.
|
||||
if matches!(style, 11 | 14) && (k.abs() > 0.5 || hd != 0.0 || vd != 0.0) {
|
||||
let geo = Buf { img: Image::new(0, 0), offset: b.offset, scale: b.scale };
|
||||
let map = e.g.upload_image(&effectcraft_effects::warp_inverse_map(ctx, &geo, b.img.width, b.img.height))?;
|
||||
b.img = run(e, "fxw_remap", &Params::default(), &b.img, Some(&map), None);
|
||||
return Some(b);
|
||||
}
|
||||
let c = b.to_px([ctx.layer_size[0] * 0.5, ctx.layer_size[1] * 0.5]);
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [0, style.min(14), vertical as u32, 0];
|
||||
|
|
|
|||
|
|
@ -23,13 +23,13 @@
|
|||
//! effect's exact steps (padding, box-blur radii, parameter conversions); chains of them are
|
||||
//! uploaded and read back once. Each family lives in its own module with its own WGSL file
|
||||
//! (`fx_color`, `fx_depth`, `fx_distort`, `fx_extra`, `fx_generate`, `fx_key`, `fx_light`,
|
||||
//! `fx_lut`, `fx_noise`, `fx_sim`, `fx_stylize`, `fx_text`, `fx_time`, `fx_tone`,
|
||||
//! `fx_lut`, `fx_noise`, `fx_particles`, `fx_sim`, `fx_stylize`, `fx_text`, `fx_time`, `fx_tone`,
|
||||
//! `fx_transition`, `fx_vr`, `fx_warp`); settings a kernel cannot match fall back to the CPU
|
||||
//! (`catalog::gpu_supported`, or `None` from the family's `apply`).
|
||||
//! The 3D Channel effects read the layer's aux channels as an extra texture (`fx_depth`), the
|
||||
//! colour-management effects interpret colour programs built next to the CPU effects
|
||||
//! (`fx_lut`), and simulations keep their state on the CPU and rasterise its per-frame plan here
|
||||
//! (`fx_sim`).
|
||||
//! (`fx_sim`, `fx_particles`).
|
||||
//!
|
||||
//! Working textures come from a pool (`context::Pool`): a frame allocates several full-frame
|
||||
//! RGBA f32 images per layer, and creating and zeroing those cost more than compositing a small
|
||||
|
|
@ -64,6 +64,7 @@ mod fx_key;
|
|||
mod fx_light;
|
||||
mod fx_lut;
|
||||
mod fx_noise;
|
||||
mod fx_particles;
|
||||
mod fx_sim;
|
||||
mod fx_stylize;
|
||||
mod fx_text;
|
||||
|
|
@ -332,6 +333,8 @@ mod tests_fx_lut;
|
|||
#[cfg(test)]
|
||||
mod tests_fx_noise;
|
||||
#[cfg(test)]
|
||||
mod tests_fx_particles;
|
||||
#[cfg(test)]
|
||||
mod tests_fx_sim;
|
||||
#[cfg(test)]
|
||||
mod tests_fx_stylize;
|
||||
|
|
|
|||
|
|
@ -444,6 +444,47 @@ fn fxl_run(c0: vec3<f32>) -> vec3<f32> {
|
|||
c = fxl_file(o, sh, c, interp, inv);
|
||||
pc += 5u;
|
||||
}
|
||||
case 9u: {
|
||||
// Affine: matrix × (c + pre) + post (a config's MatrixTransform, RangeTransform).
|
||||
let r0 = fxl_d3(pc + 1u);
|
||||
let r1 = fxl_d3(pc + 4u);
|
||||
let r2 = fxl_d3(pc + 7u);
|
||||
let d = c + fxl_d3(pc + 10u);
|
||||
c = vec3<f32>(r0.x * d.x + r0.y * d.y + r0.z * d.z, r1.x * d.x + r1.y * d.y + r1.z * d.z, r2.x * d.x + r2.y * d.y + r2.z * d.z)
|
||||
+ fxl_d3(pc + 13u);
|
||||
pc += 16u;
|
||||
}
|
||||
case 10u: {
|
||||
// ExponentTransform: max(c, 0) ^ p.
|
||||
let e = fxl_d3(pc + 1u);
|
||||
c = vec3<f32>(powz(max(c.x, 0.0), e.x), powz(max(c.y, 0.0), e.y), powz(max(c.z, 0.0), e.z));
|
||||
pc += 4u;
|
||||
}
|
||||
case 11u: {
|
||||
// Log / LogAffineTransform: base, log slope, log offset, lin slope, lin offset,
|
||||
// inverse, ln of f64's smallest normal (the CPU's floor).
|
||||
let base = data[pc + 1u];
|
||||
let ls = fxl_d3(pc + 2u);
|
||||
let lo = fxl_d3(pc + 5u);
|
||||
let lis = fxl_d3(pc + 8u);
|
||||
let lio = fxl_d3(pc + 11u);
|
||||
let lnb = log(base);
|
||||
if (data[pc + 14u] != 0.0) {
|
||||
for (var i = 0u; i < 3u; i++) {
|
||||
c[i] = (exp((c[i] - lo[i]) / ls[i] * lnb) - lio[i]) / lis[i];
|
||||
}
|
||||
} else {
|
||||
for (var i = 0u; i < 3u; i++) {
|
||||
let v = lis[i] * c[i] + lio[i];
|
||||
var ln = data[pc + 15u];
|
||||
if (v >= 1.1754944e-38) {
|
||||
ln = log(v);
|
||||
}
|
||||
c[i] = ls[i] * (ln / lnb) + lo[i];
|
||||
}
|
||||
}
|
||||
pc += 16u;
|
||||
}
|
||||
default: {
|
||||
return c;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,255 @@
|
|||
// ---------------------------------------------------------------- particle render passes (fx_particles.rs)
|
||||
//
|
||||
// fxp_raster composites tiled items (the layout of fxm_raster: data = tile offsets, item
|
||||
// indices, then the records, u[0].w floats each, the last four the item's integer pixel bounds)
|
||||
// over a base image (aux), so large plans draw in several passes. Every shading step mirrors
|
||||
// effects::sim / sim2 / sim3.
|
||||
|
||||
// One texel of a Layer Map frame in the atlas (src): transparent outside the frame, as
|
||||
// Image::sample_bilinear reads outside an image. Record fields 14..17 = atlas x, y, frame w, h.
|
||||
fn fxp_atlas_get(at: u32, x: i32, y: i32) -> vec4<f32> {
|
||||
if (x < 0 || y < 0 || x >= i32(data[at + 16u]) || y >= i32(data[at + 17u])) {
|
||||
return vec4<f32>(0.0);
|
||||
}
|
||||
return textureLoad(src, vec2<i32>(x + i32(data[at + 14u]), y + i32(data[at + 15u])), 0);
|
||||
}
|
||||
|
||||
// Image::sample_bilinear of a Layer Map frame.
|
||||
fn fxp_atlas_bilinear(at: u32, x: f32, y: f32) -> vec4<f32> {
|
||||
let fx = x - 0.5;
|
||||
let fy = y - 0.5;
|
||||
let x0 = floor(fx);
|
||||
let y0 = floor(fy);
|
||||
let tx = fx - x0;
|
||||
let ty = fy - y0;
|
||||
let xi = i32(x0);
|
||||
let yi = i32(y0);
|
||||
let a = fxp_atlas_get(at, xi, yi);
|
||||
let b = fxp_atlas_get(at, xi + 1, yi);
|
||||
let c = fxp_atlas_get(at, xi, yi + 1);
|
||||
let d = fxp_atlas_get(at, xi + 1, yi + 1);
|
||||
let top = a + (b - a) * tx;
|
||||
let bot = c + (d - c) * tx;
|
||||
return top + (bot - top) * ty;
|
||||
}
|
||||
|
||||
// sim3::blit_layer at a pixel centre. Record: kind, x, y, sin, cos (rotation), scale x, scale y,
|
||||
// buffer scale, alpha, frame layer size w, h, frame buffer scale, offset x, y, atlas x, y,
|
||||
// frame pixels w, h.
|
||||
fn fxp_blit(at: u32, x: f32, y: f32) -> FxmShade {
|
||||
let bs = data[at + 7u];
|
||||
let dx = (x - data[at + 1u]) / bs;
|
||||
let dy = (y - data[at + 2u]) / bs;
|
||||
let sa = data[at + 3u];
|
||||
let ca = data[at + 4u];
|
||||
let sw = data[at + 9u];
|
||||
let sh = data[at + 10u];
|
||||
let u = (dx * ca + dy * sa) / data[at + 5u] + sw * 0.5;
|
||||
let v = (-dx * sa + dy * ca) / data[at + 6u] + sh * 0.5;
|
||||
if (u < 0.0 || v < 0.0 || u >= sw || v >= sh) {
|
||||
return fxm_none();
|
||||
}
|
||||
let fs = data[at + 11u];
|
||||
let s = fxp_atlas_bilinear(at, u * fs + data[at + 12u], v * fs + data[at + 13u]);
|
||||
if (s.w <= 0.0) {
|
||||
return fxm_none();
|
||||
}
|
||||
return FxmShade(true, s * data[at + 8u]);
|
||||
}
|
||||
|
||||
// sim2::foam_disc's coverage and disc coordinates (u, v clamped to −1..1) at a pixel centre for
|
||||
// the disc (x, y, r) in record fields 1..3: (u, v, coverage); coverage ≤ 0 = outside.
|
||||
fn fxp_disc(at: u32, x: f32, y: f32) -> vec3<f32> {
|
||||
let r = data[at + 3u];
|
||||
let u = (x - data[at + 1u]) / r;
|
||||
let v = (y - data[at + 2u]) / r;
|
||||
let d = sqrt(u * u + v * v);
|
||||
let cov = clamp((1.0 - d) * r + 0.5, 0.0, 1.0);
|
||||
return vec3<f32>(clamp(u, -1.0, 1.0), clamp(v, -1.0, 1.0), cov);
|
||||
}
|
||||
|
||||
// Foam's User Defined texture filling a bubble (src = the texture layer's buffer). Record:
|
||||
// kind, x, y, r, sin, cos (rotation), fade, texture layer size w, h, buffer scale, offset x, y.
|
||||
fn fxp_foam_tex(at: u32, x: f32, y: f32) -> FxmShade {
|
||||
let q = fxp_disc(at, x, y);
|
||||
if (q.z <= 0.0) {
|
||||
return fxm_none();
|
||||
}
|
||||
let sr = data[at + 4u];
|
||||
let cr = data[at + 5u];
|
||||
let tu = q.x * cr + q.y * sr;
|
||||
let tv = -q.x * sr + q.y * cr;
|
||||
let tx = (tu * 0.5 + 0.5) * data[at + 7u];
|
||||
let ty = (tv * 0.5 + 0.5) * data[at + 8u];
|
||||
let ts = data[at + 9u];
|
||||
let px = sample_bilinear(src, tx * ts + data[at + 10u], ty * ts + data[at + 11u]);
|
||||
return FxmShade(true, px * data[at + 6u] * q.z);
|
||||
}
|
||||
|
||||
// Foam's Environment Map reflection on a bubble (src = the map fitted to the buffer). Record:
|
||||
// kind, x, y, r, convergence, strength, buffer w, h.
|
||||
fn fxp_foam_env(at: u32, x: f32, y: f32) -> FxmShade {
|
||||
let q = fxp_disc(at, x, y);
|
||||
if (q.z <= 0.0) {
|
||||
return fxm_none();
|
||||
}
|
||||
let bx = data[at + 1u];
|
||||
let by = data[at + 2u];
|
||||
let conv = data[at + 4u];
|
||||
var ex = bx;
|
||||
var ey = by;
|
||||
if (conv < 1.0) {
|
||||
ex = bx * conv + (q.x * 0.5 + 0.5) * data[at + 6u] * (1.0 - conv);
|
||||
ey = by * conv + (q.y * 0.5 + 0.5) * data[at + 7u] * (1.0 - conv);
|
||||
}
|
||||
let e = sample_bilinear(src, ex, ey);
|
||||
let rim = min(q.x * q.x + q.y * q.y, 1.0);
|
||||
let k = data[at + 5u] * (0.3 + 0.7 * rim);
|
||||
return FxmShade(true, vec4<f32>(e.xyz * k, 0.0) * q.z);
|
||||
}
|
||||
|
||||
// Items over the base (aux). u[0] = (kind: 0 sprites, 1 Layer Map frames, 2 Foam texture discs,
|
||||
// 3 Foam reflection discs, 4 pieces without a back texture; mode: 0 over, 1 add, 2 Foam's disc
|
||||
// compositing; items offset; item stride); u[1].x = tiles across.
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxp_raster(@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 tile = u32(p.y / 16) * P.u[1].x + u32(p.x / 16);
|
||||
let start = u32(data[tile]);
|
||||
let end = u32(data[tile + 1u]);
|
||||
let stride = P.u[0].w;
|
||||
let x = f32(p.x) + 0.5;
|
||||
let y = f32(p.y) + 0.5;
|
||||
var d = textureLoad(aux, p, 0);
|
||||
for (var k = start; k < end; k++) {
|
||||
let at = P.u[0].z + u32(data[k]) * stride;
|
||||
let bb = at + stride - 4u;
|
||||
if (f32(p.x) < data[bb] || f32(p.y) < data[bb + 1u] || f32(p.x) > data[bb + 2u] || f32(p.y) > data[bb + 3u]) {
|
||||
continue;
|
||||
}
|
||||
var s: FxmShade;
|
||||
switch (P.u[0].x) {
|
||||
case 0u: {
|
||||
s = fxm_sprite(at, x, y);
|
||||
}
|
||||
case 1u: {
|
||||
s = fxp_blit(at, x, y);
|
||||
}
|
||||
case 2u: {
|
||||
s = fxp_foam_tex(at, x, y);
|
||||
}
|
||||
case 3u: {
|
||||
s = fxp_foam_env(at, x, y);
|
||||
}
|
||||
default: {
|
||||
s = fxm_piece(at, x, y);
|
||||
}
|
||||
}
|
||||
if (!s.ok) {
|
||||
continue;
|
||||
}
|
||||
let c = s.c;
|
||||
switch (P.u[0].y) {
|
||||
case 1u: {
|
||||
d = vec4<f32>(d.xyz + c.xyz, min(d.w + c.w, 1.0));
|
||||
}
|
||||
case 2u: {
|
||||
let kk = 1.0 - c.w;
|
||||
d = vec4<f32>(c.xyz + d.xyz * kk, min(c.w + d.w * kk, 1.0));
|
||||
}
|
||||
default: {
|
||||
d = c + d * (1.0 - c.w);
|
||||
}
|
||||
}
|
||||
}
|
||||
textureStore(out, p, d);
|
||||
}
|
||||
|
||||
// Foam's Draft + Flow Map view: the flow map (aux) at half strength under the bubbles (src).
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxp_under(@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 o = textureLoad(src, p, 0);
|
||||
let f = textureLoad(aux, p, 0);
|
||||
textureStore(out, p, o + f * 0.5 * (1.0 - o.w));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Curl Noise (noise2::curl_noise)
|
||||
//
|
||||
// u[0] = (seed, flow steps, wrap edges); f[0] = (offset x, offset y, scale, complexity);
|
||||
// f[1] = (cos rotation, sin rotation, evolution · 4, step length).
|
||||
|
||||
// The fBm potential at each pixel (in .x).
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxp_curl_pot(@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 dx = f32(p.x) + 0.5 - P.f[0].x;
|
||||
let dy = f32(p.y) + 0.5 - P.f[0].y;
|
||||
let cr = P.f[1].x;
|
||||
let sr = P.f[1].y;
|
||||
let u = (dx * cr + dy * sr) / P.f[0].z;
|
||||
let v = (-dx * sr + dy * cr) / P.f[0].z;
|
||||
textureStore(out, p, vec4<f32>(fxs_fbm(u, v, P.f[1].z, P.u[0].x, P.f[0].w), 0.0, 0.0, 0.0));
|
||||
}
|
||||
|
||||
// The curl of the potential (src): (∂ψ/∂y, −∂ψ/∂x) · scale, edges clamped.
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxp_curl_vel(@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 gx = (tex_get_clamped(src, p.x + 1, p.y).x - tex_get_clamped(src, p.x - 1, p.y).x) * 0.5;
|
||||
let gy = (tex_get_clamped(src, p.x, p.y + 1).x - tex_get_clamped(src, p.x, p.y - 1).x) * 0.5;
|
||||
textureStore(out, p, vec4<f32>(gy * P.f[0].z, -gx * P.f[0].z, 0.0, 0.0));
|
||||
}
|
||||
|
||||
// The Flow Field view of the velocities (src).
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxp_curl_view(@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 v = textureLoad(src, p, 0);
|
||||
textureStore(out, p, vec4<f32>(clamp(0.5 + v.x * 0.5, 0.0, 1.0), clamp(0.5 + v.y * 0.5, 0.0, 1.0), 0.5, 1.0));
|
||||
}
|
||||
|
||||
// The layer (src) advected backwards along the velocities (aux).
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxp_curl_flow(@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 sx = f32(p.x) + 0.5;
|
||||
var sy = f32(p.y) + 0.5;
|
||||
let dt = P.f[1].w;
|
||||
for (var i = 0u; i < P.u[0].y; i++) {
|
||||
let vel = sample_bilinear_clamped(aux, sx, sy);
|
||||
sx -= vel.x * dt;
|
||||
sy -= vel.y * dt;
|
||||
}
|
||||
if (P.u[0].z != 0u) {
|
||||
let w = f32(dims.x);
|
||||
let h = f32(dims.y);
|
||||
sx = sx - w * floor(sx / w);
|
||||
sy = sy - h * floor(sy / h);
|
||||
}
|
||||
textureStore(out, p, sample_bilinear_clamped(src, sx, sy));
|
||||
}
|
||||
|
|
@ -35,6 +35,33 @@ fn ftm_mix(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|||
textureStore(out, p, a + (textureLoad(aux, p, 0) - a) * P.f[0].x);
|
||||
}
|
||||
|
||||
// Timewarp's Matte Layer with Pixel Motion (time_fx::layered): src = foreground, aux =
|
||||
// background; u[0].x = Show (1 foreground, 2 background, 3 the foreground's alpha as a matte,
|
||||
// else foreground over background).
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn ftm_layer(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!ftm_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let f = textureLoad(src, p, 0);
|
||||
let b = textureLoad(aux, p, 0);
|
||||
var o = f + b * (1.0 - f.w);
|
||||
switch (P.u[0].x) {
|
||||
case 1u: {
|
||||
o = f;
|
||||
}
|
||||
case 2u: {
|
||||
o = b;
|
||||
}
|
||||
case 3u: {
|
||||
o = vec4<f32>(f.w, f.w, f.w, 1.0);
|
||||
}
|
||||
default: {}
|
||||
}
|
||||
textureStore(out, p, o);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Time Difference (time_fx::time_difference)
|
||||
// src = the layer now, aux = the target. u[0] = (absolute difference, alpha channel);
|
||||
// f[0].x = contrast gain.
|
||||
|
|
|
|||
|
|
@ -530,3 +530,20 @@ fn fxw_cartoon(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|||
}
|
||||
textureStore(out, p, vec4<f32>(o * a, a));
|
||||
}
|
||||
|
||||
// Warp along an inverse map solved on the CPU (aux: source x, y, valid), util::remap without
|
||||
// edge clamping.
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxw_remap(@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 m = textureLoad(aux, p, 0);
|
||||
var o = vec4<f32>(0.0);
|
||||
if (m.z > 0.0) {
|
||||
o = sample_bilinear(src, m.x, m.y);
|
||||
}
|
||||
textureStore(out, p, o);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -173,3 +173,71 @@ fn lumetri_luts_and_looks() {
|
|||
&[("basicCorrection/inputLut", e(1)), ("basicCorrection/inputLutFile", s(&cube_1d())), ("creative/look", e(1)), ("creative/lookFile", s(&cube_3d()))],
|
||||
);
|
||||
}
|
||||
|
||||
/// A custom config: exponent, matrix + offset, group (matrix, log), log-affine (inverse),
|
||||
/// range, CDL and data spaces, and a display.
|
||||
const CONFIG: &str = r#"ocio_profile_version: 2
|
||||
roles:
|
||||
scene_linear: lin
|
||||
displays:
|
||||
Monitor:
|
||||
- !<View> {name: Video, colorspace: gamma22}
|
||||
- !<View> {name: Log, colorspace: log2}
|
||||
colorspaces:
|
||||
- !<ColorSpace>
|
||||
name: lin
|
||||
- !<ColorSpace>
|
||||
name: gamma22
|
||||
to_scene_reference: !<ExponentTransform> {value: [2.2, 2.2, 2.2, 1]}
|
||||
- !<ColorSpace>
|
||||
name: half
|
||||
from_reference: !<MatrixTransform> {matrix: [0.5, 0.1, 0, 0, 0, 0.5, 0, 0, 0.05, 0, 0.5, 0, 0, 0, 0, 1], offset: [0.1, 0.1, 0.1, 0]}
|
||||
- !<ColorSpace>
|
||||
name: log2
|
||||
from_reference: !<GroupTransform>
|
||||
children:
|
||||
- !<MatrixTransform> {matrix: [2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 1]}
|
||||
- !<LogTransform> {base: 2}
|
||||
- !<ColorSpace>
|
||||
name: affine
|
||||
to_reference: !<LogAffineTransform> {base: 10, logSideSlope: 0.5, logSideOffset: 0.2, linSideSlope: 2, linSideOffset: 0.01, direction: inverse}
|
||||
- !<ColorSpace>
|
||||
name: range
|
||||
from_reference: !<RangeTransform> {min_in_value: 0.1, max_in_value: 0.9, min_out_value: 0, max_out_value: 1}
|
||||
- !<ColorSpace>
|
||||
name: graded
|
||||
to_reference: !<CDLTransform> {slope: [1.2, 1, 0.9], offset: [0.02, 0, -0.01], power: [1.1, 0.9, 1], sat: 1.2}
|
||||
- !<ColorSpace>
|
||||
name: raw
|
||||
isdata: true
|
||||
"#;
|
||||
|
||||
#[test]
|
||||
fn ocio_custom_config() {
|
||||
let cfg = [("config", e(1)), ("configFile", s(CONFIG))];
|
||||
for (a, z) in [
|
||||
("gamma22", "lin"),
|
||||
("lin", "gamma22"),
|
||||
("gamma22", "half"),
|
||||
("half", "lin"),
|
||||
("lin", "log2"),
|
||||
("log2", "graded"),
|
||||
("affine", "lin"),
|
||||
("lin", "affine"),
|
||||
("range", "gamma22"),
|
||||
("lin", "range"),
|
||||
("graded", "lin"),
|
||||
("raw", "half"),
|
||||
("lin", "missing"),
|
||||
] {
|
||||
for dir in 0..2 {
|
||||
let vals = [cfg.as_slice(), &[("sourceName", s(a)), ("destinationName", s(z)), ("direction", e(dir))]].concat();
|
||||
effect_case("ec.color.ociocolorspace", &vals);
|
||||
}
|
||||
}
|
||||
for (view, dir) in [("Video", 0), ("Log", 0), ("Log", 1)] {
|
||||
let vals = [cfg.as_slice(), &[("sourceName", s("half")), ("displayName", s("Monitor")), ("viewName", s(view)), ("direction", e(dir))]].concat();
|
||||
effect_case("ec.color.ociodisplay", &vals);
|
||||
}
|
||||
effect_case("ec.color.ociocolorspace", &[("config", e(1)), ("configFile", s("/missing.ocio"))]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,145 @@
|
|||
//! GPU particle render passes (`fx_particles`) vs the CPU effects (the oracle): the frame's
|
||||
//! plan is shared, the raster runs on each side. Direct on a buffer (full and half resolution)
|
||||
//! and composited at 8 and 32 bpc (`tests_fx_sim`'s harness).
|
||||
|
||||
use effectcraft_keyframe::Value;
|
||||
|
||||
use crate::tests::n;
|
||||
use crate::tests_fx_sim::{case, composited, e, off, on};
|
||||
|
||||
fn v2(x: f64, y: f64) -> Value {
|
||||
Value::Vec2([x, y])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn particle_world_types_and_modes() {
|
||||
for kind in 0..9 {
|
||||
case(
|
||||
"ec.sim.ccparticleworld",
|
||||
&[("birthRate", n(1.5)), ("particle/particleType", e(kind)), ("particle/transferMode", e(kind % 4)), ("particle/birthSize", n(0.3))],
|
||||
1.0,
|
||||
0.0,
|
||||
);
|
||||
}
|
||||
for anim in [1, 2, 5, 6, 8] {
|
||||
case("ec.sim.ccparticleworld", &[("physics/animation", e(anim)), ("particle/particleType", e(1)), ("particle/sizeVariation", n(40.0))], 0.8, 0.0);
|
||||
}
|
||||
// Nothing born yet: the layer as is.
|
||||
case("ec.sim.ccparticleworld", &[("birthRate", n(0.0))], 0.0, 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn particle_systems_ii_types_and_modes() {
|
||||
for kind in 0..9 {
|
||||
case("ec.sim.ccparticlesystems2", &[("birthRate", n(3.0)), ("particle/particleType", e(kind)), ("particle/transferMode", e(kind % 4))], 1.0, 0.0);
|
||||
}
|
||||
case("ec.sim.ccparticlesystems2", &[("physics/animation", e(4)), ("producer/radiusX", n(8.0)), ("physics/direction", n(70.0))], 1.3, 0.0);
|
||||
}
|
||||
|
||||
/// A Particle Playground grid (cannon off) plus `more`.
|
||||
fn grid(more: &[(&'static str, Value)]) -> Vec<(&'static str, Value)> {
|
||||
let mut g = vec![
|
||||
("cannonEnabled", off()),
|
||||
("gridEnabled", on()),
|
||||
("grid/gridPosition", v2(35.0, 22.0)),
|
||||
("grid/gridWidth", n(40.0)),
|
||||
("grid/gridHeight", n(20.0)),
|
||||
("grid/particlesAcross", n(5.0)),
|
||||
("grid/particlesDown", n(3.0)),
|
||||
("grid/gridParticleRadius", n(3.0)),
|
||||
("gravity/gravityForce", n(20.0)),
|
||||
];
|
||||
g.extend(more.iter().cloned());
|
||||
g
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn particle_playground() {
|
||||
case("ec.sim.particleplayground", &[], 1.0, 0.0);
|
||||
case(
|
||||
"ec.sim.particleplayground",
|
||||
&[("cannon/cannonPosition", v2(20.0, 40.0)), ("cannon/particlesPerSecond", n(150.0)), ("cannon/velocity", n(90.0)), ("gravity/gravityForce", n(40.0))],
|
||||
1.2,
|
||||
0.0,
|
||||
);
|
||||
case("ec.sim.particleplayground", &grid(&[]), 0.7, 0.0);
|
||||
case(
|
||||
"ec.sim.particleplayground",
|
||||
&grid(&[("options/gridText", Value::Str("Ab C".into())), ("grid/gridParticleRadius", n(12.0)), ("options/autoOrientRotation", on())]),
|
||||
0.6,
|
||||
0.0,
|
||||
);
|
||||
case("ec.sim.particleplayground", &grid(&[("repel/repelForce", n(5.0)), ("repel/repelForceRadius", n(12.0))]), 0.9, 0.0);
|
||||
composited(
|
||||
"ec.sim.particleplayground",
|
||||
&[
|
||||
("cannonEnabled", off()),
|
||||
("exploderEnabled", on()),
|
||||
("layerExploder/explodeLayer", Value::Layer(Some(0))),
|
||||
("layerExploder/radiusOfNewParticles", n(3.0)),
|
||||
],
|
||||
0.5,
|
||||
0.0,
|
||||
);
|
||||
// Layer Map: every grid particle shows the other layer, at frames set by the offset type.
|
||||
for (kind, offset) in [(0, 0.0), (2, 0.5), (3, 1.0)] {
|
||||
composited(
|
||||
"ec.sim.particleplayground",
|
||||
&grid(&[
|
||||
("layerMapEnabled", on()),
|
||||
("layerMap/layerMapLayer", Value::Layer(Some(0))),
|
||||
("layerMap/timeOffsetType", e(kind)),
|
||||
("layerMap/timeOffset", n(offset)),
|
||||
("grid/particlesAcross", n(3.0)),
|
||||
("grid/particlesDown", n(2.0)),
|
||||
]),
|
||||
0.8,
|
||||
0.0,
|
||||
);
|
||||
}
|
||||
composited(
|
||||
"ec.sim.particleplayground",
|
||||
&grid(&[
|
||||
("mapperEnabled", on()),
|
||||
("persistentPropertyMapper/useLayerAsMap", Value::Layer(Some(0))),
|
||||
("persistentPropertyMapper/mapRedTo", e(8)),
|
||||
("persistentPropertyMapper/redMin", n(-50.0)),
|
||||
("persistentPropertyMapper/redMax", n(50.0)),
|
||||
]),
|
||||
0.7,
|
||||
0.0,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ball_action_pixel_polly_scatterize() {
|
||||
case("ec.sim.ccballaction", &[], 0.0, 0.0);
|
||||
case(
|
||||
"ec.sim.ccballaction",
|
||||
&[("scatter", n(20.0)), ("rotationAxis", e(6)), ("rotation", n(40.0)), ("twistProperty", e(3)), ("twistAngle", n(90.0))],
|
||||
0.0,
|
||||
0.0,
|
||||
);
|
||||
case("ec.sim.ccballaction", &[("gridSpacing", n(2.0)), ("ballSize", n(80.0)), ("instabilityState", n(45.0)), ("twistProperty", e(2))], 0.0, 0.0);
|
||||
for object in 0..4 {
|
||||
case("ec.sim.ccpixelpolly", &[("object", e(object)), ("gridSpacing", n(6.0))], 0.6, 0.0);
|
||||
}
|
||||
case("ec.sim.ccpixelpolly", &[("enableDepthSort", off()), ("spinning", n(90.0)), ("gravity", n(2.0))], 1.0, 0.0);
|
||||
case("ec.sim.ccpixelpolly", &[("startTime", n(1.0))], 0.5, 0.0);
|
||||
case("ec.sim.ccscatterize", &[("scatter", n(10.0))], 0.0, 0.0);
|
||||
case("ec.sim.ccscatterize", &[("rightTwist", n(60.0)), ("leftTwist", n(-30.0)), ("transferMode", e(1)), ("scatter", n(4.0))], 0.0, 0.0);
|
||||
case("ec.sim.ccscatterize", &[], 0.0, 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn curl_noise() {
|
||||
case("ec.noise.curlnoise", &[], 0.0, 0.0);
|
||||
case("ec.noise.curlnoise", &[("view", e(1))], 0.0, 0.0);
|
||||
case(
|
||||
"ec.noise.curlnoise",
|
||||
&[("edgeBehavior", e(1)), ("steps", n(8.0)), ("displacementAmount", n(60.0)), ("scale", n(20.0)), ("rotation", n(30.0)), ("evolution", n(90.0))],
|
||||
0.0,
|
||||
0.0,
|
||||
);
|
||||
case("ec.noise.curlnoise", &[("displacementAmount", n(0.0))], 0.0, 0.0);
|
||||
}
|
||||
|
|
@ -10,20 +10,20 @@ use effectcraft_time::Tick;
|
|||
|
||||
use crate::tests::{Scene, c, check, compare_at, diff, gpu, n, opts, pattern, set};
|
||||
|
||||
fn e(v: u32) -> Value {
|
||||
pub(crate) fn e(v: u32) -> Value {
|
||||
Value::Enum(v)
|
||||
}
|
||||
|
||||
fn on() -> Value {
|
||||
pub(crate) fn on() -> Value {
|
||||
Value::Bool(true)
|
||||
}
|
||||
|
||||
fn off() -> Value {
|
||||
pub(crate) fn off() -> Value {
|
||||
Value::Bool(false)
|
||||
}
|
||||
|
||||
/// The effect alone on a buffer at layer time `t` (`allow` = fraction of pixels over 1e-3).
|
||||
fn direct(id: &str, vals: &[(&str, Value)], t: f64, allow: f64) {
|
||||
pub(crate) fn direct(id: &str, vals: &[(&str, Value)], t: f64, allow: f64) {
|
||||
let Some(g) = gpu() else { return };
|
||||
let spec = effectcraft_effects::find(id).unwrap();
|
||||
let size = [70.0, 44.0];
|
||||
|
|
@ -53,7 +53,7 @@ fn direct(id: &str, vals: &[(&str, Value)], t: f64, allow: f64) {
|
|||
|
||||
/// A footage layer with the effect over a background and a second footage layer (the target of
|
||||
/// `Value::Layer(Some(0))` parameters), composited at 8 and 32 bpc at comp time `t`.
|
||||
fn composited(id: &str, vals: &[(&str, Value)], t: f64, allow: f64) {
|
||||
pub(crate) fn composited(id: &str, vals: &[(&str, Value)], t: f64, allow: f64) {
|
||||
for depth in [BitDepth::Bpc8, BitDepth::Bpc32] {
|
||||
let mut s = Scene::new(depth);
|
||||
let bg = s.solid([0.15, 0.1, 0.2], 97, 61);
|
||||
|
|
@ -72,7 +72,7 @@ fn composited(id: &str, vals: &[(&str, Value)], t: f64, allow: f64) {
|
|||
}
|
||||
}
|
||||
|
||||
fn case(id: &str, vals: &[(&str, Value)], t: f64, allow: f64) {
|
||||
pub(crate) fn case(id: &str, vals: &[(&str, Value)], t: f64, allow: f64) {
|
||||
direct(id, vals, t, allow);
|
||||
composited(id, vals, t, allow);
|
||||
}
|
||||
|
|
@ -154,9 +154,31 @@ fn caustics() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn cpu_only_views_fall_back() {
|
||||
// Shatter's wireframe views and Foam's User Defined texture render on the CPU; composited
|
||||
// frames still match.
|
||||
composited("ec.sim.shatter", &[], 1.0, 0.0);
|
||||
fn shatter_wireframes_and_foam_extras() {
|
||||
// Shatter's wireframe views (Wireframe Front View is the default) draw lines as sprites.
|
||||
for view in 1..5 {
|
||||
case("ec.sim.shatter", &[("view", e(view))], 1.0, 0.0);
|
||||
}
|
||||
case("ec.sim.shatter", &[("view", e(4)), ("force2/force2Radius", n(0.3)), ("shape/pattern", e(3))], 1.5, 0.0);
|
||||
// Foam's User Defined texture, Environment Map and flow-map preview.
|
||||
composited("ec.sim.foam", &[("view", e(2)), ("rendering/bubbleTexture", e(5)), ("rendering/bubbleTextureLayer", Value::Layer(Some(0)))], 1.0, 0.0);
|
||||
composited(
|
||||
"ec.sim.foam",
|
||||
&[("view", e(2)), ("rendering/bubbleTexture", e(5)), ("rendering/bubbleTextureLayer", Value::Layer(Some(0))), ("rendering/bubbleOrientation", e(2))],
|
||||
1.4,
|
||||
0.0,
|
||||
);
|
||||
composited("ec.sim.foam", &[("view", e(2)), ("rendering/environmentMap", Value::Layer(Some(0))), ("rendering/reflectionStrength", n(0.5))], 1.2, 0.0);
|
||||
composited(
|
||||
"ec.sim.foam",
|
||||
&[
|
||||
("view", e(2)),
|
||||
("rendering/environmentMap", Value::Layer(Some(0))),
|
||||
("rendering/reflectionStrength", n(0.8)),
|
||||
("rendering/reflectionConvergence", n(1.0)),
|
||||
],
|
||||
1.2,
|
||||
0.0,
|
||||
);
|
||||
composited("ec.sim.foam", &[("view", e(1)), ("flowMap/flowMap", Value::Layer(Some(0))), ("flowMap/flowMapSteepness", n(0.5))], 1.0, 0.0);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -111,6 +111,13 @@ fn timewarp() {
|
|||
);
|
||||
moving("ec.time.timewarp", &[("speed", n(55.0)), ("tuning/buildFromOneImage", on())], None);
|
||||
moving("ec.time.timewarp", &[("speed", n(47.0))], Some("warpLayer"));
|
||||
// A Matte Layer renders on the CPU (catalog::gpu_supported).
|
||||
moving("ec.time.timewarp", &[("speed", n(47.0)), ("show", e(0))], Some("matteLayer"));
|
||||
// A Matte Layer: every method and Show, the luminance channels, motion blur.
|
||||
for method in 0..3 {
|
||||
for show in 0..4 {
|
||||
moving("ec.time.timewarp", &[("method", e(method)), ("speed", n(47.0)), ("show", e(show))], Some("matteLayer"));
|
||||
}
|
||||
}
|
||||
moving("ec.time.timewarp", &[("speed", n(39.0)), ("matteChannel", e(2))], Some("matteLayer"));
|
||||
moving("ec.time.timewarp", &[("speed", n(43.0)), ("matteChannel", e(1)), ("motionBlur/enableMotionBlur", on())], Some("matteLayer"));
|
||||
moving("ec.time.timewarp", &[("method", e(1)), ("speed", n(52.0)), ("tuning/buildFromOneImage", on()), ("show", e(1))], Some("matteLayer"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,15 +28,12 @@ fn warp_styles() {
|
|||
effect_case("ec.distort.warp", &[("warpStyle", e(4)), ("bend", n(80.0)), ("verticalDistortion", n(-35.0))]);
|
||||
effect_case("ec.distort.warp", &[("warpStyle", e(11)), ("bend", n(-50.0))]);
|
||||
effect_case("ec.distort.warp", &[("warpStyle", e(14)), ("bend", n(-35.0))]);
|
||||
// Fisheye / Twist with a distortion render on the CPU (see catalog::gpu_supported).
|
||||
let spec = effectcraft_effects::find("ec.distort.warp").unwrap();
|
||||
let mut params = effectcraft_effects::Params {
|
||||
values: spec.params.iter().map(|ps| (ps.id.to_string(), effectcraft_effects::default_value(ps, [70.0, 44.0]))).collect(),
|
||||
};
|
||||
params.values.insert("warpStyle".into(), e(14));
|
||||
params.values.insert("verticalDistortion".into(), n(-35.0));
|
||||
let ctx = effectcraft_effects::EffectCtx { params: ¶ms, time: 0.0, layer_size: [70.0, 44.0], seed: 0, adjustment: false, env: Default::default() };
|
||||
assert!(!effectcraft_effects::catalog::gpu_supported("ec.distort.warp", &ctx));
|
||||
// Strong Fisheye / Twist bends and either with a distortion: the CPU's f64 inverse map.
|
||||
for (style, bend) in [(11, 90.0), (11, -100.0), (14, 75.0), (14, -100.0)] {
|
||||
effect_case("ec.distort.warp", &[("warpStyle", e(style)), ("bend", n(bend))]);
|
||||
}
|
||||
effect_case("ec.distort.warp", &[("warpStyle", e(14)), ("verticalDistortion", n(-35.0))]);
|
||||
effect_case("ec.distort.warp", &[("warpStyle", e(11)), ("warpAxis", e(1)), ("bend", n(60.0)), ("horizontalDistortion", n(30.0))]);
|
||||
effect_case("ec.distort.warp", &[("warpStyle", e(3)), ("bend", n(0.0)), ("horizontalDistortion", n(-40.0)), ("verticalDistortion", n(25.0))]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -209,7 +209,7 @@ Preserve Transparency / blend kernels (a 3D track matte renders solo through the
|
|||
Environment Light Background layers draw their sky in a kernel (`adv_sky`, from
|
||||
`Renderer::sky_draw`) before the rest of the run, in Classic and Advanced 3D comps. Wireframe
|
||||
outlines draw on the GPU from the CPU's pixel list (`Renderer::wireframe_pixels`). GPU effects
|
||||
(`effects::GPU_EFFECTS`, 236 of them: blurs, colour correction, keying incl. Key Light, mattes,
|
||||
(`effects::GPU_EFFECTS`, 243 of them: blurs, colour correction, keying incl. Key Light, mattes,
|
||||
channel, stylize, distortion and warps (Warp, Bezier Warp, Smear, Reshape, CC Bend It, CC Page
|
||||
Turn, CC Bender, CC Blobbylize), Cartoon, bevels, shapes, the transitions (CC light family, CC
|
||||
transitions, Block Dissolve), perspective (Radial Shadow, CC Cylinder / Sphere / Spotlight /
|
||||
|
|
@ -217,7 +217,10 @@ Environment, 3D Glasses), generators, noise, grain, text (Numbers, Timecode), ti
|
|||
extra texture), Immersive Video, colour management (colour programs from
|
||||
`effects::color_program`, LUTs in a storage buffer) and the simulations' render passes (the
|
||||
simulation stays on the CPU and hands its plan — sprites, pieces, blobs, grids — to a tiled
|
||||
rasteriser); see
|
||||
rasteriser; the particle effects too, `gpu::fx_particles`: CC Particle World / Systems II,
|
||||
Particle Playground with its Layer Map frames in one atlas, CC Ball Action, CC Pixel Polly, CC
|
||||
Scatterize; plans too large for one item table draw in several passes over the previous one);
|
||||
see
|
||||
[effects.md](effects.md)) repeat the CPU effect's steps (padding, box radii, parameters, hashes)
|
||||
as kernels, in one module per family (`gpu::fx_*` with `shaders/fx_*.wgsl`); consecutive GPU
|
||||
effects run as one chain with one upload and one readback. Statistics that need the whole frame
|
||||
|
|
@ -229,10 +232,12 @@ combine them on the GPU; Time Displacement's per-pixel times and Timewarp's moti
|
|||
(`time_fx::timewarp_plan`) are computed on the CPU. Numbers and Timecode rasterise their glyph
|
||||
coverage on the CPU (`textfx::text_layer`) and composite it on the GPU. Other layers an effect
|
||||
reads (gradients, reveals, environments) are fitted on the CPU (`util::fit_layer`) and uploaded.
|
||||
Settings a kernel cannot match render on the CPU (`effects::catalog::gpu_supported`; e.g. Warp's
|
||||
Fisheye and Twist past 50 % or with a distortion, whose Newton inverse wanders chaotically near
|
||||
the crease, and Timewarp with a Matte Layer); Card Wipe (the 3D card renderer of Card Dance and
|
||||
Shatter) stays on the CPU. Tests render
|
||||
Where f32 cannot follow the CPU's f64, the CPU solves the part that needs it and the GPU uses
|
||||
the result: Warp's Fisheye and Twist past 50 % or with a distortion (a Newton inverse that
|
||||
wanders chaotically near the crease) sample the CPU's inverse map (`warp_inverse_map`). Settings
|
||||
a kernel cannot match still render on the CPU (`effects::catalog::gpu_supported`: Cell
|
||||
Pattern's HQ variants, Turbulent Displace's locked pinning and the controls in
|
||||
`CPU_ONLY_CONTROLS`). Tests render
|
||||
scenes on both paths and compare them (≤ 1/255 at 8 bpc, ≤ 1e-3 at 32 bpc); they skip without an
|
||||
adapter. Working textures (premultiplied RGBA f32) come from a pool: a released texture is reused
|
||||
once every encoder that could still read it has been submitted, transparent inputs share one
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
EffectCraft ships 306 effects with 3009 parameters in total, grouped into the same categories as the Effects & Presets panel. Every effect is our own implementation, written from public behaviour descriptions and standard image-processing literature. Parameter names, order, popup options, units, defaults and ranges follow the reference application so that projects, expressions and muscle memory carry over.
|
||||
|
||||
- **GPU**: 236 effects also run on the GPU compositor with identical results.
|
||||
- **GPU**: 243 effects also run on the GPU compositor with identical results.
|
||||
- **32**: 306 effects process 32-bit float (HDR, overbright) pixels without clamping.
|
||||
- **Status**: 306 implemented in full, 0 partial (what is missing is listed).
|
||||
|
||||
|
|
@ -265,7 +265,7 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
|||
| Effect | Id | Params | GPU | 32 | Status |
|
||||
|---|---|---:|:-:|:-:|---|
|
||||
| Add Grain | `ec.noise.addgrain` | 32 | GPU | 32 | Implemented |
|
||||
| Curl Noise | `ec.noise.curlnoise` | 10 | | 32 | Implemented |
|
||||
| Curl Noise | `ec.noise.curlnoise` | 10 | GPU | 32 | Implemented |
|
||||
| Dust & Scratches | `ec.noise.dustscratches` | 3 | GPU | 32 | Implemented |
|
||||
| Fractal Noise | `ec.noise.fractal` | 26 | GPU | 32 | Implemented |
|
||||
| Match Grain | `ec.noise.matchgrain` | 37 | | 32 | Implemented |
|
||||
|
|
@ -317,22 +317,22 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
|||
|
||||
| Effect | Id | Params | GPU | 32 | Status |
|
||||
|---|---|---:|:-:|:-:|---|
|
||||
| CC Ball Action | `ec.sim.ccballaction` | 8 | | 32 | Implemented |
|
||||
| CC Ball Action | `ec.sim.ccballaction` | 8 | GPU | 32 | Implemented |
|
||||
| CC Bubbles | `ec.sim.ccbubbles` | 7 | GPU | 32 | Implemented |
|
||||
| CC Drizzle | `ec.sim.ccdrizzle` | 14 | GPU | 32 | Implemented |
|
||||
| CC Hair | `ec.sim.cchair` | 20 | GPU | 32 | Implemented |
|
||||
| CC Mr. Mercury | `ec.sim.ccmrmercury` | 23 | GPU | 32 | Implemented |
|
||||
| CC Particle Systems II | `ec.sim.ccparticlesystems2` | 21 | | 32 | Implemented |
|
||||
| CC Particle World | `ec.sim.ccparticleworld` | 30 | | 32 | Implemented |
|
||||
| CC Pixel Polly | `ec.sim.ccpixelpolly` | 10 | | 32 | Implemented |
|
||||
| CC Particle Systems II | `ec.sim.ccparticlesystems2` | 21 | GPU | 32 | Implemented |
|
||||
| CC Particle World | `ec.sim.ccparticleworld` | 30 | GPU | 32 | Implemented |
|
||||
| CC Pixel Polly | `ec.sim.ccpixelpolly` | 10 | GPU | 32 | Implemented |
|
||||
| CC Rainfall | `ec.sim.ccrainfall` | 14 | GPU | 32 | Implemented |
|
||||
| CC Scatterize | `ec.sim.ccscatterize` | 4 | | 32 | Implemented |
|
||||
| CC Scatterize | `ec.sim.ccscatterize` | 4 | GPU | 32 | Implemented |
|
||||
| CC Snowfall | `ec.sim.ccsnowfall` | 19 | GPU | 32 | Implemented |
|
||||
| CC Star Burst | `ec.sim.ccstarburst` | 6 | GPU | 32 | Implemented |
|
||||
| Card Dance | `ec.sim.carddance` | 53 | GPU | 32 | Implemented |
|
||||
| Caustics | `ec.sim.caustics` | 28 | GPU | 32 | Implemented |
|
||||
| Foam | `ec.sim.foam` | 36 | GPU | 32 | Implemented |
|
||||
| Particle Playground | `ec.sim.particleplayground` | 92 | | 32 | Implemented |
|
||||
| Particle Playground | `ec.sim.particleplayground` | 92 | GPU | 32 | Implemented |
|
||||
| Shatter | `ec.sim.shatter` | 60 | GPU | 32 | Implemented |
|
||||
| Wave World | `ec.sim.waveworld` | 35 | GPU | 32 | Implemented |
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ How close EffectCraft is to After Effects 2026, feature by feature, and how much
|
|||
| Unweighted | ≈ 97% (half-credit) / 99% (fractions) |
|
||||
| P0 / P1 / P2 | ≈ 99% / 96% / 90% (half-credit); 99.8% / 99.2% / 97.5% (fractions) |
|
||||
| Features done / partial / missing | 87 / 5 / 0 of 92 |
|
||||
| **Effects** | **306** effects (all After Effects 2026 effects), every one implemented in full ([effects.md](effects.md)); 236 run on the GPU |
|
||||
| **Effects** | **306** effects (all After Effects 2026 effects), every one implemented in full ([effects.md](effects.md)); 243 run on the GPU |
|
||||
| Disabled menu entries left | 1, on purpose: Import ▸ Vanishing Point (.vpe), whose format has no public specification |
|
||||
| Remaining work | ≈ 9–11 agent-hours at the pace measured so far (≈ 29 on the conservative audit scale); ≈ 6–7 without the learned-model items |
|
||||
| **Wall-clock estimate** | **≈ 2–2.5 hours** with five agents in parallel; ≈ 1.5 hours for 100% of P0 + P1 |
|
||||
|
|
@ -19,7 +19,7 @@ Partial features:
|
|||
|
||||
| id | Tier | Done | What is missing |
|
||||
|---|---|---|---|
|
||||
| EFF-5 GPU effects | P1 | 0.97 | GPU kernels for the audio-driven effects and the stepped particle systems' sprite pass (the CC light / transition families, Numbers, Timecode and Time effects landed in M13.22; 3D Channel, the VR family, OCIO / LUT and the simulations' render passes in M13.23); custom `.ocio` configs, Shatter's wireframe views and Foam's texture / environment / flow-map extras render on the CPU |
|
||||
| EFF-5 GPU effects | P1 | 0.98 | GPU kernels for the audio-driven effects (the CC light / transition families, Numbers, Timecode and Time effects landed in M13.22; 3D Channel, the VR family, OCIO / LUT and the simulations' render passes in M13.23; the particle effects' render passes — CC Particle World / Systems II, Particle Playground, CC Ball Action, CC Pixel Polly, CC Scatterize — and Curl Noise in M13.29, which also moved Shatter's wireframe views, Foam's texture / environment / flow-map extras, Timewarp with a Matte Layer, strong Warp Fisheye / Twist bends and custom `.ocio` configs onto the GPU); Cell Pattern's HQ variants, Turbulent Displace's locked pinning and the `CPU_ONLY_CONTROLS` render on the CPU |
|
||||
| WEB-1 Web app | P0 | 0.97 | GPU effects in Render Queue / analysis job workers (they render on their CPU); layer buffers in the browser's disk cache (frames only); threads inside one engine instance (the decided design is one engine instance per worker; a threaded build needs nightly `build-std`). M13.24 added GPU effects and the GPU compositor in the frame workers (their own WebGPU devices, deferred readbacks: frames render in passes; Backend Auto per comp), the disk cache in the Origin Private File System (written by the workers with sync access handles, LRU under the settings' limit, served after a reload), the storage manager (Settings ▸ Disk ▸ Browser Storage, `storage.*`) and fixed a WGSL constant Chrome rejected (it disabled every GPU kernel in Chrome) |
|
||||
| UI-7 Home screen | P1 | 0.95 | A "start from a template" gallery |
|
||||
| MSK-4 Roto Brush | P2 | 0.85 | Segmentation is classical (graph cut + optical flow), not a learned model |
|
||||
|
|
@ -156,7 +156,7 @@ the disk cache in the formats wave; the 3D Camera Tracker in M12.6.)
|
|||
| Text | ≈ 96% | 0.3 | no extruded strokes (M13.12: the Variable Font Axes animator re-spaces the text — advances follow the animated axes; M13.6: variable font axes in the character style — `layer.setText variations`, the Character panel's Variable Font Axes fields — shape with HVAR / gvar advances and draw at that design-space position; OpenType features — stylistic sets, discretionary ligatures, contextual / stylistic alternates, swash, titling, ordinals, fractions, figure styles, true small caps / all small caps and superior / inferior glyphs with faux fallback — per character with the Character panel's OpenType popup and `text.fontFeatures` landed in M13.2; vertical Roman / Tate-Chu-Yoko, forced LTR paragraphs, caret on animated and path text, Variable Font Axes and Lottie style runs landed in M13.5; extruded, bevelled text in M7.6; per-character styles, paragraph settings, on-canvas editing and the `sourceText` style API in M9.9–M9.10) |
|
||||
| Web | 97% | 0.2 | GPU effects in Render Queue / analysis workers (job workers render on their CPU), layer buffers in the browser's disk cache (frames only), no shared-memory threads inside one engine instance (the decided design is one engine instance per worker; a threaded build needs nightly `build-std`) (browser storage, Web Audio, Web Worker renders/analyses, WebGPU viewer and offline install landed in M15.2; viewer frames in frame workers fed by project diffs, Roto Brush propagation in a worker, non-blocking `wait: true` jobs and a browser Media Browser (File System Access folders, browser storage) in M13.10; WEB-1 in M13.24: GPU effects and the GPU compositor in the frame workers on their own WebGPU devices with deferred readbacks (frames render in passes; Backend Auto per comp), the disk cache in the Origin Private File System (written by the workers with sync access handles, LRU under the settings' limit, served after a reload), the storage manager (Settings ▸ Disk ▸ Browser Storage, `storage.info` / `storage.persist` / `storage.clear`), and a WGSL constant Chrome rejected (it disabled every GPU kernel in Chrome) fixed) |
|
||||
| 3D | 88% | 5.5 | multi-view layouts, the Extended Viewer for Advanced 3D comps (Classic 3D Extended Viewer landed in M13.5 UI completion); collapsed precomps of another size seen through the parent's camera render (fixed in M13.2); stereo rigs, orbit nulls, lights controlled by the camera, cameras/lights from glTF models, environment backgrounds, Advanced 3D motion blur, blend modes and track mattes landed in M7.7; Classic 3D iris-shaped bokeh with highlights, progressive depth of field on tilted layers and the focus-link commands landed in M13.5; Advanced 3D depth of field with the iris and highlight options, collapsed precomps as real Advanced 3D geometry and extruded text/shape strokes landed in M13.8; Advanced 3D (glTF/OBJ models, primitives, extruded text and shapes, PBR, image-based light, shadow maps, GPU rasteriser) in M7.4–M7.6; Advanced 3D end to end on the GPU (motion blur, iris depth of field, compositing) in M13.11 |
|
||||
| Effects | ≈ 85% | 3.0 | GPU versions of the remaining effects (236 run on the GPU since M13.23, EFF-5: M13.23 added the 3D Channel and Immersive Video families, Apply Color LUT, the OCIO effects, Color Profile Converter and the simulations' render passes (Card Wipe included); M13.22 added the CC light family, the CC transitions, Block Dissolve, Radial Shadow, CC Bender / Blobbylize / Cylinder / Sphere / Spotlight / Environment, 3D Glasses, Numbers, Timecode and the time effects joined the 166 of M13.13; audio-driven effects and the stepped particle systems still render on the CPU) and the missing controls listed as partial in [effects.md](effects.md) (every After Effects effect exists since M9.11, M12.5 and M12.6; parameter names, order, twirl-downs, popups, units and defaults were aligned in M9.12) |
|
||||
| Effects | ≈ 85% | 3.0 | GPU versions of the remaining effects (243 run on the GPU since M13.29, EFF-5: M13.29 added the particle effects (CC Particle World / Systems II, Particle Playground, CC Ball Action, CC Pixel Polly, CC Scatterize, Curl Noise) and removed the fallbacks of Shatter's wireframe views, Foam's extras, Timewarp's Matte Layer, strong Warp Fisheye / Twist bends and custom `.ocio` configs; M13.23 added the 3D Channel and Immersive Video families, Apply Color LUT, the OCIO effects, Color Profile Converter and the simulations' render passes (Card Wipe included); M13.22 added the CC light family, the CC transitions, Block Dissolve, Radial Shadow, CC Bender / Blobbylize / Cylinder / Sphere / Spotlight / Environment, 3D Glasses, Numbers, Timecode and the time effects joined the 166 of M13.13; audio-driven effects still render on the CPU) and the missing controls listed as partial in [effects.md](effects.md) (every After Effects effect exists since M9.11, M12.5 and M12.6; parameter names, order, twirl-downs, popups, units and defaults were aligned in M9.12) |
|
||||
| Interface | 75% | 5.0 | more Learn tutorials and pixel-level fidelity of dialogs (the Home ▸ Templates gallery (eight original built-in templates, user templates from File ▸ Save as Template…) and View ▸ Simulate Output ▸ My Custom RGB… landed in M13.25; Timeline layer reordering by drag, a non-snapping viewer pan, a working rename field and twirl arrows, and the full set of property reveal shortcuts — double presses, Alt+Shift keyframes, Ctrl+` — landed in M13.17–M13.20; the Home ▸ Learn tab with interactive tutorials and a UI fidelity pass landed in M13.5 UI completion; visual editors for Lumetri RGB / hue-saturation curves, Colorama's output cycle wheel, Glow's colour map and Reshape's correspondence points (viewer handles), a scrolling Preview panel and AE-style Composition / Timeline tabs (close, label swatch, viewer lock) landed in M13.10; Timeline outline and Project panel columns scroll horizontally, the Layer Style dialog, ROI resize handles, Pan Behind snapping and 3D Reference Axes landed in M13.5; native macOS menu bar, Timeline columns/search/reveal-add, Home screen with recent projects and all AE workspaces landed; viewer rulers/snapping/channels/snapshots landed in M0.13) |
|
||||
| Project | ≈ 68% | 5.5 | auto-save, folder moves, OCIO displays beyond the built-in tone map (Color Engine with OCIO/ACES working spaces, HDR compand/tone mapping, Rec. 2100 PQ/HLG output, Feet + Frames, display colour management, Simulate Output and the locked viewer landed in M7.7; proxies and Interpret Footage fields / pixel aspect / alpha guess landed: PRJ-8, PRJ-3) |
|
||||
| Masks & roto | 74% | 5.0 | Roto Brush's learned (3.0) segmentation model (variable-width mask feather points with the Mask Feather tool landed in M13.5; mask tracking and Mask Interpolation landed in M6.6; Roto Brush & Refine Edge with graph-cut segmentation, flow propagation, edge matting, decontamination and Freeze in M6.7) |
|
||||
|
|
@ -460,6 +460,33 @@ composited at 8 and 32 bpc:
|
|||
default to the layer). Shatter's wireframe views and Foam's User Defined texture,
|
||||
Environment Map and flow-map preview render on the CPU.
|
||||
|
||||
## Update: M13.29 GPU effects, part D (EFF-5)
|
||||
|
||||
7 more effects run on the GPU (243 in all), and five CPU fallbacks are gone. Each case is
|
||||
checked against the CPU oracle (≤ 1/255 at 8 bpc, ≤ 1e-3 at 32 bpc) directly on a buffer (full
|
||||
and half resolution) and composited at 8 and 32 bpc; the CPU refactors behind the shared plans
|
||||
are pinned bit for bit by golden hashes (`crates/effects/tests/particle_golden.rs`,
|
||||
`timewarp_golden.rs`, `sim_golden.rs`).
|
||||
|
||||
- **Particle render passes** (`gpu::fx_particles`): CC Particle World and CC Particle Systems II
|
||||
(every particle type and transfer mode; the stepped simulation stays on the CPU or the GPU
|
||||
`ParticleSim`, sorted and shaded as the CPU does), Particle Playground (cannon, grid, text
|
||||
strokes, layer exploder, property mappers, repel, wall; the Layer Map particles' frames are
|
||||
packed into one atlas), CC Ball Action, CC Pixel Polly and CC Scatterize (their plans read the
|
||||
frame back once: the particles take the layer's colours and, for Ball Action's twist, its
|
||||
luminance). Plans too large for one item table draw in several passes, each over the previous
|
||||
one, with the same per-pixel arithmetic.
|
||||
- **Curl Noise**: the fBm potential, its curl and the streamline trace as three kernels.
|
||||
- **Former fallbacks**: Shatter's wireframe views (the default view) draw their lines as
|
||||
sprites; Foam's User Defined texture, Environment Map and flow-map preview draw over the
|
||||
bubble sprites; Timewarp with a Matte Layer carries the masked frames and the foreground's and
|
||||
background's own vectors in its plan and builds both layers on the GPU; custom `.ocio` configs
|
||||
compile to the colour program (matrix and offset, exponent, log / log-affine, range, CDL, file
|
||||
LUTs, groups, inverses). Warp's Fisheye / Twist past 50 % or with a distortion keeps the CPU's
|
||||
f64 Newton solve (f32, even with more iterations, lands on other roots near the crease): the
|
||||
CPU solves the inverse map from the geometry alone and the GPU samples along it, so the chain
|
||||
stays on the GPU without a readback.
|
||||
|
||||
## Highest-value gaps, in order
|
||||
|
||||
1. ~~On-canvas text editing and per-character styles~~ (landed: M9.9–M9.10).
|
||||
|
|
|
|||
Loading…
Reference in New Issue