mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-30 21:16:52 -06:00
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:
co-authored by
Claude Opus 5.5
parent
b9f09869eb
commit
1c855c3e07
@@ -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 })
|
||||
}
|
||||
+87
-32
@@ -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;
|
||||
|
||||
+22
-16
@@ -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))]);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user