mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-31 07:38:24 +00:00
Merge pull request #1610 from marcoarnulfo/fix/particle-world-texture-layer
fix(effects): let CC Particle World's Textured Square use a Texture Layer
This commit is contained in:
7 files changed
+173
-10
No files matched your search
@@ -92,8 +92,8 @@ const CPU_ONLY_CONTROLS: &[(&str, &[&str])] = &[
|
||||
];
|
||||
|
||||
/// Whether the GPU kernel of effect `id` covers the instance's current settings (see
|
||||
/// `CPU_ONLY_CONTROLS`; plus Cell Pattern's HQ variants and Turbulent Displace's locked
|
||||
/// pinning).
|
||||
/// `CPU_ONLY_CONTROLS`; plus Cell Pattern's HQ variants, Turbulent Displace's locked
|
||||
/// pinning and CC Particle World's motion trails and textured squares).
|
||||
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,
|
||||
@@ -101,6 +101,8 @@ pub fn gpu_supported(id: &str, ctx: &crate::EffectCtx) -> bool {
|
||||
// #1195: CC Particle World's motion trails (the averaged shutter samples) render on the
|
||||
// CPU kernel until the GPU particle pass supports them.
|
||||
"ec.sim.ccparticleworld" if ctx.env.shutter.filter(|(a, _, _)| *a > 0.0).is_some() => return false,
|
||||
// #1465: Textured Square's Texture Layer is drawn on the CPU only.
|
||||
"ec.sim.ccparticleworld" if crate::sim::particle_world_textured(ctx) => return false,
|
||||
_ => {}
|
||||
}
|
||||
let Some((_, controls)) = CPU_ONLY_CONTROLS.iter().find(|(e, _)| *e == id) else { return true };
|
||||
@@ -224,6 +226,10 @@ mod tests {
|
||||
assert!(!super::gpu_supported("ec.sim.ccparticleworld", &ctx));
|
||||
}
|
||||
assert!(ok("ec.sim.ccparticleworld", &[]));
|
||||
// #1465: the Texture Layer keeps CC Particle World on the CPU only when Textured Square draws it.
|
||||
let tex = ("particle/texture/textureLayer", Value::Layer(Some(1)));
|
||||
assert!(ok("ec.sim.ccparticleworld", &[tex.clone(), ("particle/particleType", Value::Enum(2))]));
|
||||
assert!(!ok("ec.sim.ccparticleworld", &[tex, ("particle/particleType", Value::Enum(8))]));
|
||||
assert!(ok("ec.generate.cellpattern", &[("cellPattern", Value::Enum(3))]));
|
||||
assert!(!ok("ec.generate.cellpattern", &[("cellPattern", Value::Enum(9))]));
|
||||
assert!(!ok("ec.distort.turbulentdisplace", &[("pinning", Value::Enum(9))]));
|
||||
|
||||
@@ -32,6 +32,7 @@ use engine::{MERCURY, PS2, PW_EFFECT_CAMERA, Stepped, rate_history, sim_key, sim
|
||||
#[cfg(test)]
|
||||
pub(crate) use engine::{PState, PW_ANIMS, Phys, simulate};
|
||||
pub(crate) use engine::{SPS, steps_at};
|
||||
pub(crate) use particle_fx::particle_world_textured;
|
||||
pub use particle_fx::{Blob, mercury_blobs};
|
||||
#[cfg(test)]
|
||||
use particle_fx::{PW_RATE, mercury_phys, ps2_phys, pw_phys};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::engine::{MERCURY, PS2, PState, PW_EFFECT_CAMERA, Phys, norm3, particle_look, pw_state, type_sprite};
|
||||
use super::sprites::{Acc, Post, Sprite, SpritePlan, raster, splat};
|
||||
use super::sprites::{Acc, Post, Sprite, SpritePlan, aa, raster, splat, sprite_bbox};
|
||||
use super::{Buf, EffectCtx, Image, IntoParallelRefIterator, ParallelIterator, SimCache, Value, unpremul};
|
||||
|
||||
// ------------------------------------------------------------------ CC Particle World
|
||||
@@ -35,20 +35,44 @@ pub(super) fn pw_phys(ctx: &EffectCtx) -> Phys {
|
||||
}
|
||||
}
|
||||
|
||||
/// CC Particle World's Particle Type ▸ Textured Square.
|
||||
const PW_TEXTURED_SQUARE: u32 = 8;
|
||||
/// CC Particle World's Particle ▸ Texture ▸ Texture Layer.
|
||||
pub(super) const PW_TEXTURE_LAYER: &str = "particle/texture/textureLayer";
|
||||
|
||||
/// Whether CC Particle World draws its Texture Layer on the particles (Textured Square with a
|
||||
/// layer chosen). Only the CPU does: `catalog::gpu_supported` keeps this case off the GPU.
|
||||
pub(crate) fn particle_world_textured(ctx: &EffectCtx) -> bool {
|
||||
ctx.params.e("particle/particleType") == PW_TEXTURED_SQUARE && ctx.params.get(PW_TEXTURE_LAYER).and_then(Value::as_layer).is_some()
|
||||
}
|
||||
|
||||
pub(super) fn particle_world(ctx: &EffectCtx, b: Buf) -> Buf {
|
||||
// The texture at the current comp time, shared by every shutter sample. A layer the host
|
||||
// can't give (the effect's own layer with its effects, a reference cycle) leaves the
|
||||
// squares in the particle colour. Authored buffers (text, shapes) span only their content:
|
||||
// covering the whole layer keeps the clamped sampling from stretching their edge pixels.
|
||||
let tex = particle_world_textured(ctx).then(|| ctx.layer_param(PW_TEXTURE_LAYER, true)).flatten().map(|mut t| {
|
||||
t.buf.cover(t.size);
|
||||
t
|
||||
});
|
||||
let (w, h) = (b.img.width, b.img.height);
|
||||
let draw = |plan: &SpritePlan| match &tex {
|
||||
Some(t) => splat_textured(w, h, &plan.sprites, plan.acc, t),
|
||||
None => splat(w, h, &plan.sprites, plan.acc),
|
||||
};
|
||||
// #1195: with the comp's motion-blur shutter open, particles draw as motion trails: the
|
||||
// sprite fields of samples across the shutter are averaged, as Transform averages its
|
||||
// matrix samples. (The GPU compositor runs this case on the CPU: catalog::gpu_supported.)
|
||||
if let Some((angle, phase, n)) = ctx.env.shutter.filter(|(a, _, _)| *a > 0.0) {
|
||||
let n = (n.clamp(2, 64) as usize).max(2);
|
||||
let fd = 1.0 / ctx.fps().max(0.001);
|
||||
let mut acc = Image::new(b.img.width, b.img.height);
|
||||
let mut acc = Image::new(w, h);
|
||||
let mut post: Option<Post> = None;
|
||||
for k in 0..n {
|
||||
let t = ctx.time + (phase + angle * k as f64 / (n - 1) as f64) / 360.0 * fd;
|
||||
let sub = EffectCtx { params: ctx.params, time: t, layer_size: ctx.layer_size, seed: ctx.seed, adjustment: ctx.adjustment, env: ctx.env };
|
||||
if let Some(plan) = particle_world_plan(&sub, &b) {
|
||||
let fx = splat(b.img.width, b.img.height, &plan.sprites, plan.acc);
|
||||
let fx = draw(&plan);
|
||||
acc.data.iter_mut().zip(&fx.data).for_each(|(a, o)| (0..4).for_each(|c| a[c] += o[c]));
|
||||
post.get_or_insert(plan.post);
|
||||
}
|
||||
@@ -60,11 +84,38 @@ pub(super) fn particle_world(ctx: &EffectCtx, b: Buf) -> Buf {
|
||||
};
|
||||
}
|
||||
match particle_world_plan(ctx, &b) {
|
||||
Some(plan) => plan.finish(b),
|
||||
Some(plan) => {
|
||||
let fx = draw(&plan);
|
||||
plan.finish_with(b, fx)
|
||||
}
|
||||
None => b,
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw square sprites into a new transparent image of `w`×`h`, each showing the layer `tex`
|
||||
/// stretched over the square (turned with it) at the sprite's opacity.
|
||||
fn splat_textured(w: u32, hgt: u32, sprites: &[Sprite], acc: Acc, tex: &crate::LayerPixels) -> Image {
|
||||
let mut img = Image::new(w, hgt);
|
||||
let (tw, th) = (tex.size[0], tex.size[1]);
|
||||
let shade = |s: &Sprite, x: f32, y: f32| {
|
||||
// As [`sprite_shade`]'s Square, sub-pixel sprites included.
|
||||
let (r, fade) = if s.r < 0.5 { (0.5, s.r / 0.5) } else { (s.r, 1.0) };
|
||||
let (sn, cs) = s.rot.sin_cos();
|
||||
let (dx, dy) = (x - s.x, y - s.y);
|
||||
let (ux, uy) = (dx * cs + dy * sn, -dx * sn + dy * cs);
|
||||
let e = r * 0.75;
|
||||
let a = aa(e - ux.abs()) * aa(e - uy.abs()) * s.c[3] * fade;
|
||||
if a <= 1e-6 {
|
||||
return None;
|
||||
}
|
||||
let (u, v) = (((ux / e + 1.0) * 0.5).clamp(0.0, 1.0) as f64, ((uy / e + 1.0) * 0.5).clamp(0.0, 1.0) as f64);
|
||||
let (px, py) = tex.buf.to_px([u * tw, v * th]);
|
||||
Some(tex.buf.img.sample_bilinear_clamped(px, py).map(|c| c * a))
|
||||
};
|
||||
raster(&mut img, sprites, sprite_bbox, shade, acc);
|
||||
img
|
||||
}
|
||||
|
||||
/// Effect Camera ▸ FOV's default (degrees).
|
||||
pub(super) const PW_FOV: f64 = 50.0;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::common::{pct, pt, spec};
|
||||
use super::engine::{PARTICLE_TYPES, PW_ANIMS};
|
||||
use super::particle_fx::{PW_FOV, mr_mercury, particle_systems2, particle_systems2_plan, particle_world, particle_world_plan};
|
||||
use super::particle_fx::{PW_FOV, PW_TEXTURE_LAYER, mr_mercury, particle_systems2, particle_systems2_plan, particle_world, particle_world_plan};
|
||||
use super::sprites::SpritePlan;
|
||||
use super::weather::{bubbles, drizzle, rainfall, rainfall_plan, snowfall, snowfall_plan, star_burst, star_burst_plan};
|
||||
use super::{Buf, EffectCtx, EffectSpec, ParamUi, Value, col, num, p, popup, slider};
|
||||
@@ -151,6 +151,7 @@ pub fn specs() -> Vec<EffectSpec> {
|
||||
p("physics/gravityVector/gravityY", "Gravity Y", num(1.0), slider(-1.0, 1.0, -1.0, 1.0, 2)),
|
||||
p("physics/gravityVector/gravityZ", "Gravity Z", num(0.0), slider(-1.0, 1.0, -1.0, 1.0, 2)),
|
||||
p("particle/particleType", "Particle Type", Value::Enum(0), popup(PARTICLE_TYPES)),
|
||||
p(PW_TEXTURE_LAYER, "Texture Layer", Value::Layer(None), ParamUi::Layer),
|
||||
p("particle/birthSize", "Birth Size", num(0.2), slider(0.0, 10.0, 0.0, 1.0, 3)),
|
||||
p("particle/deathSize", "Death Size", num(0.2), slider(0.0, 10.0, 0.0, 1.0, 3)),
|
||||
p("particle/sizeVariation", "Size Variation", num(0.0), pct()),
|
||||
|
||||
@@ -225,7 +225,7 @@ impl Post {
|
||||
}
|
||||
}
|
||||
|
||||
fn sprite_bbox(s: &Sprite) -> Option<[f32; 4]> {
|
||||
pub(super) fn sprite_bbox(s: &Sprite) -> Option<[f32; 4]> {
|
||||
if s.c[3] <= 1e-5 || s.r <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -383,6 +383,110 @@ fn particle_world_effect_camera() {
|
||||
assert_ne!(key(¶ms), key(&moved));
|
||||
}
|
||||
|
||||
/// A host whose every layer is `img`.
|
||||
struct TexHost(Image);
|
||||
impl crate::EffectHost for TexHost {
|
||||
fn layer(&self, _: u64, _: bool) -> Option<crate::LayerPixels> {
|
||||
let s = [self.0.width as f64, self.0.height as f64];
|
||||
Some(crate::LayerPixels { buf: Buf { img: self.0.clone(), offset: [0.0; 2], scale: 1.0 }, size: s })
|
||||
}
|
||||
fn audio(&self, _: u64, _: f64, _: usize, _: u32) -> Option<Vec<f32>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// A texture whose left half is green and right half blue.
|
||||
fn two_tone_tex() -> Image {
|
||||
let mut tex = Image::new(8, 8);
|
||||
for y in 0..8 {
|
||||
for x in 0..8 {
|
||||
tex.set(x, y, if x < 4 { [0.0, 1.0, 0.0, 1.0] } else { [0.0, 0.0, 1.0, 1.0] });
|
||||
}
|
||||
}
|
||||
tex
|
||||
}
|
||||
|
||||
/// CC Particle World drawing red Textured Squares with Texture Layer `layer`.
|
||||
fn pw_textured_vals(layer: Option<u64>) -> Vec<(&'static str, Value)> {
|
||||
vec![
|
||||
("birthRate", num(1.0)),
|
||||
("extras/randomSeed", num(4.0)),
|
||||
("particle/particleType", Value::Enum(8)),
|
||||
("particle/texture/textureLayer", Value::Layer(layer)),
|
||||
("particle/birthSize", num(1.0)),
|
||||
("particle/deathSize", num(1.0)),
|
||||
("particle/birthColor", col(1.0, 0.0, 0.0)),
|
||||
("particle/deathColor", col(1.0, 0.0, 0.0)),
|
||||
]
|
||||
}
|
||||
|
||||
/// #1465: Textured Square draws the Texture Layer on each particle, not the particle colour.
|
||||
#[test]
|
||||
fn particle_world_textured_square_draws_the_texture_layer() {
|
||||
let host = TexHost(two_tone_tex());
|
||||
let vals = pw_textured_vals;
|
||||
let env = EffectEnv { host: Some(&host), ..Default::default() };
|
||||
let out = run_fx("ec.sim.ccparticleworld", &vals(Some(1)), Image::new(48, 48), 1.0, env).img;
|
||||
let seen: Vec<_> = out.data.iter().filter(|p| p[3] > 0.05).collect();
|
||||
assert!(!seen.is_empty(), "particles drawn");
|
||||
assert!(seen.iter().all(|p| p[0] < 0.02 * p[3]), "no particle colour: {:?}", seen.iter().find(|p| p[0] >= 0.02 * p[3]));
|
||||
// Both halves of the texture show: it is mapped across the square, not sampled once.
|
||||
assert!(seen.iter().any(|p| p[1] > 2.0 * p[2]) && seen.iter().any(|p| p[2] > 2.0 * p[1]), "both texture halves");
|
||||
// Without a texture layer the squares keep the particle colour.
|
||||
let plain = run_fx("ec.sim.ccparticleworld", &vals(None), Image::new(48, 48), 1.0, env).img;
|
||||
assert!(plain.data.iter().any(|p| p[3] > 0.05 && p[0] > 0.9 * p[3]));
|
||||
}
|
||||
|
||||
/// #1465 with #1195's motion trails: with the shutter open the averaged samples still show
|
||||
/// the Texture Layer, not the particle colour.
|
||||
#[test]
|
||||
fn particle_world_textured_square_motion_trails_draw_the_texture_layer() {
|
||||
let host = TexHost(two_tone_tex());
|
||||
let env = EffectEnv { host: Some(&host), frame_rate: 10.0, shutter: Some((360.0, -180.0, 8)), ..Default::default() };
|
||||
let blurred = run_fx("ec.sim.ccparticleworld", &pw_textured_vals(Some(1)), Image::new(48, 48), 1.0, env).img;
|
||||
let sharp = run_fx("ec.sim.ccparticleworld", &pw_textured_vals(Some(1)), Image::new(48, 48), 1.0, EffectEnv { shutter: None, ..env }).img;
|
||||
assert_ne!(sharp.data, blurred.data, "the shutter smears the particles");
|
||||
let seen: Vec<_> = blurred.data.iter().filter(|p| p[3] > 0.02).collect();
|
||||
assert!(!seen.is_empty(), "particles drawn");
|
||||
assert!(seen.iter().all(|p| p[0] < 0.02 * p[3]), "no particle colour: {:?}", seen.iter().find(|p| p[0] >= 0.02 * p[3]));
|
||||
assert!(seen.iter().any(|p| p[1] > 2.0 * p[2]) && seen.iter().any(|p| p[2] > 2.0 * p[1]), "both texture halves");
|
||||
}
|
||||
|
||||
/// A host whose every layer is `0` as given (a buffer that may span only its content).
|
||||
struct PixelsHost(crate::LayerPixels);
|
||||
impl crate::EffectHost for PixelsHost {
|
||||
fn layer(&self, _: u64, _: bool) -> Option<crate::LayerPixels> {
|
||||
Some(self.0.clone())
|
||||
}
|
||||
fn audio(&self, _: u64, _: f64, _: usize, _: u32) -> Option<Vec<f32>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// #1465: a Texture Layer whose buffer spans only its content (text, shapes) is mapped as
|
||||
/// the whole layer, transparent around the content, not with its edge pixels stretched.
|
||||
#[test]
|
||||
fn particle_world_texture_layer_with_a_content_sized_buffer() {
|
||||
// An 8×8 layer with a green 4×4 square in the middle: once as the full layer, once as
|
||||
// just the square's pixels.
|
||||
let mut full = Image::new(8, 8);
|
||||
for y in 2..6 {
|
||||
for x in 2..6 {
|
||||
full.set(x, y, [0.0, 1.0, 0.0, 1.0]);
|
||||
}
|
||||
}
|
||||
let content = Image::filled(4, 4, [0.0, 1.0, 0.0, 1.0]);
|
||||
let render = |buf: Buf| {
|
||||
let host = PixelsHost(crate::LayerPixels { buf, size: [8.0, 8.0] });
|
||||
let env = EffectEnv { host: Some(&host), ..Default::default() };
|
||||
run_fx("ec.sim.ccparticleworld", &pw_textured_vals(Some(1)), Image::new(48, 48), 1.0, env).img
|
||||
};
|
||||
let want = render(Buf { img: full, offset: [0.0; 2], scale: 1.0 });
|
||||
let got = render(Buf { img: content, offset: [-2.0, -2.0], scale: 1.0 });
|
||||
assert!(want.data.iter().any(|p| p[3] > 0.05), "particles drawn");
|
||||
assert_eq!(want.data, got.data, "the content-sized buffer draws as the full layer");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn particle_systems2_seek_and_direction() {
|
||||
let v = vec![
|
||||
|
||||
+2
-2
@@ -3,7 +3,7 @@
|
||||
<!-- Generated from the effect registry (crates/effects/src/catalog.rs); do not edit by hand.
|
||||
Regenerate: UPDATE_DOCS=1 cargo test -p effectcraft-effects --lib effects_doc_is_current -->
|
||||
|
||||
EffectCraft ships 308 effects with 3033 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.
|
||||
EffectCraft ships 308 effects with 3034 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**: 280 effects also run on the GPU compositor with identical results.
|
||||
- **32**: 308 effects process 32-bit float (HDR, overbright) pixels without clamping.
|
||||
@@ -325,7 +325,7 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
||||
| 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 | GPU | 32 | Implemented |
|
||||
| CC Particle World | `ec.sim.ccparticleworld` | 35 | GPU | 32 | Implemented |
|
||||
| CC Particle World | `ec.sim.ccparticleworld` | 36 | 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 | GPU | 32 | Implemented |
|
||||
|
||||
Reference in new issue
Block a user