mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-14 19:27:05 -04:00
M9.13: Foam flow map, wobble, pop velocity, bubble texture layer, orientation, environment map
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
3c586e2dba
commit
bb384fbc9c
4 files changed
+310
-59
No files matched your search
@@ -32,7 +32,6 @@ pub const PARTIAL: &[(&str, &str)] = &[
|
||||
("ec.vr.digitalglitch", "reduced control set"),
|
||||
("ec.vr.converter", "common layouts only"),
|
||||
("ec.sim.caustics", "no Sky group or light type"),
|
||||
("ec.sim.foam", "no wobble, pop velocity, custom bubble texture, environment map or flow map"),
|
||||
("ec.sim.waveworld", "no wireframe controls, dry-area rendering or ground group"),
|
||||
("ec.stylize.glow", "no Glow Operation or arbitrary colour map"),
|
||||
("ec.stylize.cartoon", "no Edge Enhancement"),
|
||||
|
||||
@@ -574,6 +574,7 @@ pub const PARAM_GROUPS: &[(&str, &str)] = &[
|
||||
("force2", "Force 2"),
|
||||
("bubbles", "Bubbles"),
|
||||
("rendering", "Rendering"),
|
||||
("flowMap", "Flow Map"),
|
||||
("heightMapControls", "Height Map Controls"),
|
||||
("simulation", "Simulation"),
|
||||
("producer1", "Producer 1"),
|
||||
|
||||
+306
-55
@@ -1213,6 +1213,8 @@ struct FoamBubble {
|
||||
v: [f32; 2],
|
||||
age: f32,
|
||||
size: f32,
|
||||
/// Spin (radians) for Physical Orientation.
|
||||
spin: f32,
|
||||
id: u32,
|
||||
}
|
||||
|
||||
@@ -1228,12 +1230,20 @@ static FOAM_CACHE: SimCache<FoamState> = SimCache::new(4);
|
||||
/// Foam steps once per frame at 30 frames per second.
|
||||
const FOAM_FPS: f64 = 30.0;
|
||||
|
||||
/// Foam's Bubble Texture options (the last uses Bubble Texture Layer).
|
||||
const FOAM_TEXTURES: [&str; 6] = ["Default Bubble", "Spit", "Bubblegum", "Dishwater", "Milky", "User Defined"];
|
||||
|
||||
/// Age (in simulation steps) at which bubble `id` pops: its lifespan, shortened at random for
|
||||
/// frail bubbles (`frailty` 0 = every bubble reaches `lifespan`, 1 = uniformly early).
|
||||
fn foam_pop_age(id: u32, lifespan: f32, frailty: f32, seed: u32) -> f32 {
|
||||
lifespan * (1.0 - frailty * (hs(id, 11, seed) * 0.5 + 0.5))
|
||||
}
|
||||
|
||||
/// Foam: bubbles born in the producer rectangle grow to their size, drift with the wind,
|
||||
/// turbulence and the Flow Map (bubbles run downhill on its luminance), repel and stick to each
|
||||
/// other, wobble, and pop at the end of their (Strength-shortened) lifespan, pushing their
|
||||
/// 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, mut b: Buf) -> Buf {
|
||||
let pr = ctx.params;
|
||||
let (lw, lh) = (ctx.layer_size[0] as f32, ctx.layer_size[1] as f32);
|
||||
@@ -1254,14 +1264,50 @@ fn foam(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let wind = pr.f("physics/windSpeed") as f32;
|
||||
let wind_dir = (pr.f("physics/windDirection") as f32).to_radians();
|
||||
let turb = pr.f("physics/turbulence") as f32;
|
||||
let wobble = pr.f("physics/wobbleAmount").max(0.0) as f32;
|
||||
let repulsion = pr.f("physics/repulsion") as f32;
|
||||
let pop_velocity = pr.f("physics/popVelocity") as f32;
|
||||
let viscosity = pr.f("physics/viscosity").clamp(0.0, 1.0) as f32;
|
||||
let sticky = pr.f("physics/stickiness").clamp(0.0, 1.0) as f32;
|
||||
let universe = pr.f("universeSize").max(0.01) as f32;
|
||||
let seed = (pr.f("randomSeed") as u32).wrapping_mul(0x68e31da4) ^ ctx.seed;
|
||||
let wvec = [wind_dir.sin() * wind * unit, -wind_dir.cos() * wind * unit];
|
||||
let key = params_key(ctx, &Buf { img: Image::new(0, 0), offset: [0.0; 2], scale: 1.0 }, 5);
|
||||
// Flow Map: the map layer's first frame, its luminance a height field (Fits the layer or
|
||||
// the whole universe).
|
||||
let flow = pr.get("flowMap/flowMap").and_then(Value::as_layer).and_then(|id| {
|
||||
let host = ctx.env.host?;
|
||||
let me = pr.get(&crate::layer_source_id("flowMap/flowMap")).is_none_or(|v| v.as_enum() != 0);
|
||||
host.layer_at(id, ctx.env.comp_time - ctx.time, me)
|
||||
});
|
||||
let steep = pr.f("flowMap/flowMapSteepness") as f32;
|
||||
let fits_universe = pr.e("flowMap/flowMapFits") == 1;
|
||||
let substeps = match pr.e("flowMap/simulationQuality") {
|
||||
1 => 2,
|
||||
2 => 4,
|
||||
_ => 1,
|
||||
};
|
||||
let height = flow.as_ref().map(|lp| {
|
||||
let img = &lp.buf.img;
|
||||
let pl = Plane::from_image(img, |px| {
|
||||
let (c, a) = unpremul(px);
|
||||
effectcraft_color::luminance(c[0], c[1], c[2]) * a
|
||||
});
|
||||
(pl, lp.buf.scale, lp.buf.offset, lp.size)
|
||||
});
|
||||
let (ux0, uy0) = (-(universe - 1.0) * 0.5 * lw, -(universe - 1.0) * 0.5 * lh);
|
||||
let (ux1, uy1) = (lw - ux0, lh - uy0);
|
||||
// Height at a layer point (0..1).
|
||||
let h_at = |x: f32, y: f32| -> f32 {
|
||||
let Some((pl, s, off, size)) = &height else { return 0.0 };
|
||||
let (fx, fy) = if fits_universe { ((x - ux0) / (ux1 - ux0), (y - uy0) / (uy1 - uy0)) } else { (x / lw, y / lh) };
|
||||
pl.sample(fx as f64 * size[0] * s + off[0], fy as f64 * size[1] * s + off[1])
|
||||
};
|
||||
let mut key = params_key(ctx, &Buf { img: Image::new(0, 0), offset: [0.0; 2], scale: 1.0 }, 5);
|
||||
if let Some((pl, ..)) = &height {
|
||||
key ^= pl.data.iter().step_by(5).fold(0u64, |a, v| a.rotate_left(3) ^ v.to_bits() as u64);
|
||||
}
|
||||
let steps = if ctx.time <= 0.0 { 0 } else { (ctx.time * FOAM_FPS + 1e-6).floor() as u64 };
|
||||
let rad = |q: &FoamBubble| q.size * unit * 3.0 * (q.age * growth).min(1.0);
|
||||
let st = FOAM_CACHE.run(key, steps, FoamState::default, |st, step| {
|
||||
let t = step as f32 / FOAM_FPS as f32;
|
||||
// Births.
|
||||
@@ -1279,73 +1325,104 @@ fn foam(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let p = [prod[0] as f32 + ux * co - uy * so, prod[1] as f32 + ux * so + uy * co];
|
||||
let v = [init_dir.sin() * init_speed * unit, -init_dir.cos() * init_speed * unit];
|
||||
let sz = (size * (1.0 + size_var * hs(id, 3, seed))).max(0.01);
|
||||
st.b.push(FoamBubble { p, v, age: 0.0, size: sz, id });
|
||||
st.b.push(FoamBubble { p, v, age: 0.0, size: sz, spin: hs(id, 12, seed) * std::f32::consts::PI, id });
|
||||
}
|
||||
// Forces: drift towards the wind, turbulence, viscosity.
|
||||
for q in st.b.iter_mut() {
|
||||
let n1 = value_noise(q.p[0] / (lw * 0.15), q.p[1] / (lw * 0.15), t * 0.5, seed) - 0.5;
|
||||
let n2 = value_noise(q.p[0] / (lw * 0.15) + 19.0, q.p[1] / (lw * 0.15), t * 0.5, seed) - 0.5;
|
||||
for k in 0..2 {
|
||||
q.v[k] += (wvec[k] - q.v[k]) * 0.1;
|
||||
q.v[k] *= 1.0 - viscosity * 0.5;
|
||||
for _ in 0..substeps {
|
||||
let k = 1.0 / substeps as f32;
|
||||
// Forces: drift towards the wind, turbulence, viscosity, the flow map's slope.
|
||||
for q in st.b.iter_mut() {
|
||||
let n1 = value_noise(q.p[0] / (lw * 0.15), q.p[1] / (lw * 0.15), t * 0.5, seed) - 0.5;
|
||||
let n2 = value_noise(q.p[0] / (lw * 0.15) + 19.0, q.p[1] / (lw * 0.15), t * 0.5, seed) - 0.5;
|
||||
for c in 0..2 {
|
||||
q.v[c] += (wvec[c] - q.v[c]) * 0.1 * k;
|
||||
q.v[c] *= 1.0 - viscosity * 0.5 * k;
|
||||
}
|
||||
q.v[0] += n1 * turb * unit * k;
|
||||
q.v[1] += n2 * turb * unit * k;
|
||||
if height.is_some() && steep != 0.0 {
|
||||
let e = unit.max(1.0);
|
||||
let gx = (h_at(q.p[0] + e, q.p[1]) - h_at(q.p[0] - e, q.p[1])) / (2.0 * e);
|
||||
let gy = (h_at(q.p[0], q.p[1] + e) - h_at(q.p[0], q.p[1] - e)) / (2.0 * e);
|
||||
// Downhill: from light (high) to dark (low).
|
||||
q.v[0] -= gx * steep * lw * 0.5 * k;
|
||||
q.v[1] -= gy * steep * lw * 0.5 * k;
|
||||
}
|
||||
// Physical Orientation: bubbles roll with their sideways motion.
|
||||
q.spin += q.v[0] / (rad(q).max(0.5)) * k;
|
||||
}
|
||||
q.v[0] += n1 * turb * unit;
|
||||
q.v[1] += n2 * turb * unit;
|
||||
}
|
||||
// Repulsion between overlapping bubbles (grid hashed, deterministic order).
|
||||
let rad = |q: &FoamBubble| q.size * unit * 3.0 * (q.age * growth).min(1.0);
|
||||
if repulsion > 0.0 && st.b.len() > 1 {
|
||||
let maxr = st.b.iter().map(rad).fold(0.0f32, f32::max).max(1.0);
|
||||
let cs = maxr * 2.0;
|
||||
let mut grid: std::collections::HashMap<(i32, i32), Vec<usize>> = std::collections::HashMap::new();
|
||||
for (i, q) in st.b.iter().enumerate() {
|
||||
grid.entry(((q.p[0] / cs).floor() as i32, (q.p[1] / cs).floor() as i32)).or_default().push(i);
|
||||
}
|
||||
let mut push = vec![[0.0f32; 2]; st.b.len()];
|
||||
for (i, q) in st.b.iter().enumerate() {
|
||||
let (gx, gy) = ((q.p[0] / cs).floor() as i32, (q.p[1] / cs).floor() as i32);
|
||||
for dy in -1..=1 {
|
||||
for dx in -1..=1 {
|
||||
let Some(list) = grid.get(&(gx + dx, gy + dy)) else { continue };
|
||||
for &j in list {
|
||||
if j <= i {
|
||||
continue;
|
||||
}
|
||||
let o = &st.b[j];
|
||||
let (ddx, ddy) = (o.p[0] - q.p[0], o.p[1] - q.p[1]);
|
||||
let d = (ddx * ddx + ddy * ddy).sqrt().max(1e-4);
|
||||
let overlap = rad(q) + rad(o) - d;
|
||||
if overlap > 0.0 {
|
||||
let f = overlap * 0.5 * repulsion.min(4.0) * (1.0 - sticky * 0.5) / d;
|
||||
push[i][0] -= ddx * f;
|
||||
push[i][1] -= ddy * f;
|
||||
push[j][0] += ddx * f;
|
||||
push[j][1] += ddy * f;
|
||||
// Repulsion between overlapping bubbles (grid hashed, deterministic order).
|
||||
if repulsion > 0.0 && st.b.len() > 1 {
|
||||
let maxr = st.b.iter().map(rad).fold(0.0f32, f32::max).max(1.0);
|
||||
let cs = maxr * 2.0;
|
||||
let mut grid: std::collections::HashMap<(i32, i32), Vec<usize>> = std::collections::HashMap::new();
|
||||
for (i, q) in st.b.iter().enumerate() {
|
||||
grid.entry(((q.p[0] / cs).floor() as i32, (q.p[1] / cs).floor() as i32)).or_default().push(i);
|
||||
}
|
||||
let mut push = vec![[0.0f32; 2]; st.b.len()];
|
||||
for (i, q) in st.b.iter().enumerate() {
|
||||
let (gx, gy) = ((q.p[0] / cs).floor() as i32, (q.p[1] / cs).floor() as i32);
|
||||
for dy in -1..=1 {
|
||||
for dx in -1..=1 {
|
||||
let Some(list) = grid.get(&(gx + dx, gy + dy)) else { continue };
|
||||
for &j in list {
|
||||
if j <= i {
|
||||
continue;
|
||||
}
|
||||
let o = &st.b[j];
|
||||
let (ddx, ddy) = (o.p[0] - q.p[0], o.p[1] - q.p[1]);
|
||||
let d = (ddx * ddx + ddy * ddy).sqrt().max(1e-4);
|
||||
let overlap = rad(q) + rad(o) - d;
|
||||
if overlap > 0.0 {
|
||||
let f = overlap * 0.5 * repulsion.min(4.0) * (1.0 - sticky * 0.5) / d * k;
|
||||
push[i][0] -= ddx * f;
|
||||
push[i][1] -= ddy * f;
|
||||
push[j][0] += ddx * f;
|
||||
push[j][1] += ddy * f;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (q, d) in st.b.iter_mut().zip(&push) {
|
||||
q.p[0] += d[0].clamp(-maxr, maxr);
|
||||
q.p[1] += d[1].clamp(-maxr, maxr);
|
||||
}
|
||||
}
|
||||
for (q, d) in st.b.iter_mut().zip(&push) {
|
||||
q.p[0] += d[0].clamp(-maxr, maxr);
|
||||
q.p[1] += d[1].clamp(-maxr, maxr);
|
||||
for q in st.b.iter_mut() {
|
||||
q.p[0] += q.v[0] * k;
|
||||
q.p[1] += q.v[1] * k;
|
||||
}
|
||||
}
|
||||
let (ux0, uy0) = (-(universe - 1.0) * 0.5 * lw, -(universe - 1.0) * 0.5 * lh);
|
||||
let (ux1, uy1) = (lw - ux0, lh - uy0);
|
||||
for q in st.b.iter_mut() {
|
||||
q.p[0] += q.v[0];
|
||||
q.p[1] += q.v[1];
|
||||
q.age += 1.0;
|
||||
}
|
||||
// Pops: bubbles past their pop age burst, pushing their neighbours away.
|
||||
let popped: Vec<([f32; 2], f32)> = st.b.iter().filter(|q| q.age >= foam_pop_age(q.id, lifespan, frailty, seed)).map(|q| (q.p, rad(q))).collect();
|
||||
if pop_velocity != 0.0 {
|
||||
for (pp, pr_) in &popped {
|
||||
let reach = (pr_ * 3.0).max(unit);
|
||||
for q in st.b.iter_mut() {
|
||||
let (dx, dy) = (q.p[0] - pp[0], q.p[1] - pp[1]);
|
||||
let d = (dx * dx + dy * dy).sqrt();
|
||||
if d > 1e-4 && d < reach {
|
||||
let f = pop_velocity * unit * (1.0 - d / reach) / d;
|
||||
q.v[0] += dx * f;
|
||||
q.v[1] += dy * f;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
st.b.retain(|q| q.age < foam_pop_age(q.id, lifespan, frailty, seed) && q.p[0] > ux0 && q.p[0] < ux1 && q.p[1] > uy0 && q.p[1] < uy1);
|
||||
});
|
||||
let view = pr.e("view");
|
||||
let zoom = pr.f("zoom").max(0.01) as f32;
|
||||
let zoom_producer = pr.b("producer/zoomProducerPoint");
|
||||
let texture = pr.e("rendering/bubbleTexture");
|
||||
let orient = pr.e("rendering/bubbleOrientation");
|
||||
let blend = pr.e("rendering/blendMode");
|
||||
let s = b.scale as f32;
|
||||
let (cx, cy) = (lw * 0.5, lh * 0.5);
|
||||
// Zoom centres on the layer, or on the producer with Zoom Producer Point.
|
||||
let (cx, cy) = if zoom_producer { (prod[0] as f32, prod[1] as f32) } else { (lw * 0.5, lh * 0.5) };
|
||||
let mut order: Vec<&FoamBubble> = st.b.iter().collect();
|
||||
if blend == 2 {
|
||||
// Solid New on Top: oldest first.
|
||||
@@ -1353,14 +1430,27 @@ fn foam(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
} else if blend == 1 {
|
||||
order.sort_by(|a, c| a.age.total_cmp(&c.age).then(a.id.cmp(&c.id)));
|
||||
}
|
||||
// Where a bubble is drawn: buffer centre, radius, rotation (radians).
|
||||
let place = |q: &FoamBubble| {
|
||||
let wob = if wobble > 0.0 { 1.0 + wobble * 0.15 * ((q.age * 0.35) + hs(q.id, 13, seed) * 6.0).sin() } else { 1.0 };
|
||||
let r = rad(q) * wob * zoom * s;
|
||||
let (bx, by) = b.to_px([(cx + (q.p[0] - cx) * zoom) as f64, (cy + (q.p[1] - cy) * zoom) as f64]);
|
||||
let rot = match orient {
|
||||
1 => q.spin,
|
||||
2 => q.v[1].atan2(q.v[0]),
|
||||
_ => 0.0,
|
||||
};
|
||||
(bx, by, r, rot)
|
||||
};
|
||||
let user_tex = (view == 2 && texture == 5).then(|| ctx.layer_param("rendering/bubbleTextureLayer", true)).flatten();
|
||||
let sprites: Vec<Sprite> = order
|
||||
.iter()
|
||||
.filter(|_| user_tex.is_none())
|
||||
.map(|q| {
|
||||
let r = q.size * unit * 3.0 * (q.age * growth).min(1.0) * zoom * s;
|
||||
let (bx, by) = b.to_px([(cx + (q.p[0] - cx) * zoom) as f64, (cy + (q.p[1] - cy) * zoom) as f64]);
|
||||
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);
|
||||
// Draft and Draft + Flow Map show the wireframe-style bubbles.
|
||||
if view != 2 {
|
||||
let mut sp = if view != 2 {
|
||||
Sprite::new(bx as f32, by as f32, r, [0.55, 0.75, 1.0, fade], Shape::Bubble)
|
||||
} else {
|
||||
let (c, shape) = match texture {
|
||||
@@ -1371,13 +1461,91 @@ fn foam(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
_ => ([1.0, 1.0, 1.0], Shape::Bubble),
|
||||
};
|
||||
Sprite::new(bx as f32, by as f32, r, [c[0], c[1], c[2], fade], shape)
|
||||
}
|
||||
};
|
||||
sp.rot = rot;
|
||||
sp
|
||||
})
|
||||
.collect();
|
||||
b.img = splat(b.img.width, b.img.height, &sprites, if blend == 0 && view == 2 { Acc::Add } else { Acc::Over });
|
||||
let mut out = splat(b.img.width, b.img.height, &sprites, if blend == 0 && view == 2 { Acc::Add } else { Acc::Over });
|
||||
// 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 strength = pr.f("rendering/reflectionStrength").clamp(0.0, 1.0) as f32;
|
||||
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
|
||||
&& 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;
|
||||
}
|
||||
});
|
||||
}
|
||||
b.img = out;
|
||||
b
|
||||
}
|
||||
|
||||
/// Add `f(u, v)` (premultiplied; u, v ∈ −1..1 across the disc) over the disc of radius `r` at
|
||||
/// (`x`, `y`) with anti-aliased edges ("over" for opaque texels, additive for alpha 0).
|
||||
fn foam_disc(out: &mut Image, x: f64, y: f64, r: f64, f: impl Fn(f64, f64) -> Px) {
|
||||
if r <= 0.0 {
|
||||
return;
|
||||
}
|
||||
let (x0, x1) = ((x - r - 1.0).floor().max(0.0) as i64, (x + r + 1.0).ceil().min(out.width as f64) as i64);
|
||||
let (y0, y1) = ((y - r - 1.0).floor().max(0.0) as i64, (y + r + 1.0).ceil().min(out.height as f64) as i64);
|
||||
for py in y0..y1 {
|
||||
for px in x0..x1 {
|
||||
let (u, v) = ((px as f64 + 0.5 - x) / r, (py as f64 + 0.5 - y) / r);
|
||||
let d = (u * u + v * v).sqrt();
|
||||
let cov = ((1.0 - d) * r + 0.5).clamp(0.0, 1.0) as f32;
|
||||
if cov <= 0.0 {
|
||||
continue;
|
||||
}
|
||||
let s = f(u.clamp(-1.0, 1.0), v.clamp(-1.0, 1.0)).map(|c| c * cov);
|
||||
let o = out.get(px, py);
|
||||
let k = 1.0 - s[3];
|
||||
out.set(px as u32, py as u32, [s[0] + o[0] * k, s[1] + o[1] * k, s[2] + o[2] * k, (s[3] + o[3] * k).min(1.0)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ specs
|
||||
|
||||
fn producer_params(k: u32, amp: f64, pos: (f64, f64)) -> Vec<crate::ParamSpec> {
|
||||
@@ -1609,6 +1777,7 @@ pub fn specs() -> Vec<EffectSpec> {
|
||||
p("producer/producerXSize", "Producer X Size", num(0.05), slider(0.0, 2.0, 0.0, 1.0, 3)),
|
||||
p("producer/producerYSize", "Producer Y Size", num(0.05), slider(0.0, 2.0, 0.0, 1.0, 3)),
|
||||
p("producer/producerOrientation", "Producer Orientation", num(0.0), ParamUi::Angle),
|
||||
p("producer/zoomProducerPoint", "Zoom Producer Point", Value::Bool(false), ParamUi::Checkbox),
|
||||
p("producer/productionRate", "Production Rate", num(1.0), slider(0.0, 50.0, 0.0, 10.0, 3)),
|
||||
p("bubbles/size", "Size", num(0.5), slider(0.0, 10.0, 0.0, 2.0, 3)),
|
||||
p("bubbles/sizeVariance", "Size Variance", num(0.5), slider(0.0, 1.0, 0.0, 1.0, 3)),
|
||||
@@ -1620,13 +1789,24 @@ pub fn specs() -> Vec<EffectSpec> {
|
||||
p("physics/windSpeed", "Wind Speed", num(0.5), slider(-100.0, 100.0, -10.0, 10.0, 3)),
|
||||
p("physics/windDirection", "Wind Direction", num(90.0), ParamUi::Angle),
|
||||
p("physics/turbulence", "Turbulence", num(0.5), slider(0.0, 10.0, 0.0, 2.0, 3)),
|
||||
p("physics/wobbleAmount", "Wobble Amount", num(0.05), slider(0.0, 10.0, 0.0, 2.0, 3)),
|
||||
p("physics/repulsion", "Repulsion", num(1.0), slider(0.0, 10.0, 0.0, 2.0, 3)),
|
||||
p("physics/popVelocity", "Pop Velocity", num(1.0), slider(-100.0, 100.0, -10.0, 10.0, 3)),
|
||||
p("physics/viscosity", "Viscosity", num(0.1), slider(0.0, 1.0, 0.0, 1.0, 3)),
|
||||
p("physics/stickiness", "Stickiness", num(0.75), slider(0.0, 1.0, 0.0, 1.0, 3)),
|
||||
p("zoom", "Zoom", num(1.0), slider(0.01, 50.0, 0.1, 5.0, 3)),
|
||||
p("universeSize", "Universe Size", num(1.0), slider(0.01, 10.0, 0.5, 3.0, 3)),
|
||||
p("rendering/blendMode", "Blend Mode", Value::Enum(0), popup(&["Transparent", "Solid Old on Top", "Solid New on Top"])),
|
||||
p("rendering/bubbleTexture", "Bubble Texture", Value::Enum(0), popup(&["Default Bubble", "Spit", "Bubblegum", "Dishwater", "Milky"])),
|
||||
p("rendering/bubbleTexture", "Bubble Texture", Value::Enum(0), popup(&FOAM_TEXTURES)),
|
||||
p("rendering/bubbleTextureLayer", "Bubble Texture Layer", Value::Layer(None), ParamUi::Layer),
|
||||
p("rendering/bubbleOrientation", "Bubble Orientation", Value::Enum(0), popup(&["Fixed", "Physical Orientation", "Bubble Velocity"])),
|
||||
p("rendering/environmentMap", "Environment Map", Value::Layer(None), ParamUi::Layer),
|
||||
p("rendering/reflectionStrength", "Reflection Strength", num(0.0), slider(0.0, 1.0, 0.0, 1.0, 3)),
|
||||
p("rendering/reflectionConvergence", "Reflection Convergence", num(0.0), slider(0.0, 1.0, 0.0, 1.0, 3)),
|
||||
p("flowMap/flowMap", "Flow Map", Value::Layer(None), ParamUi::Layer),
|
||||
p("flowMap/flowMapSteepness", "Flow Map Steepness", num(0.05), slider(-100.0, 100.0, -1.0, 1.0, 3)),
|
||||
p("flowMap/flowMapFits", "Flow Map Fits", Value::Enum(0), popup(&["Background", "Universe"])),
|
||||
p("flowMap/simulationQuality", "Simulation Quality", Value::Enum(0), popup(&["Normal", "High", "Intense"])),
|
||||
p("randomSeed", "Random Seed", num(1.0), slider(0.0, 10_000.0, 0.0, 1000.0, 0)),
|
||||
],
|
||||
foam,
|
||||
@@ -2002,4 +2182,75 @@ mod tests {
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
/// Layer 1 = a left-dark, right-light ramp; layer 2 = solid green.
|
||||
struct FoamHost;
|
||||
impl crate::EffectHost for FoamHost {
|
||||
fn layer(&self, id: u64, _: bool) -> Option<crate::LayerPixels> {
|
||||
let img = if id == 1 { ramp(32, 32) } else { Image::filled(32, 32, [0.0, 1.0, 0.0, 1.0]) };
|
||||
Some(crate::LayerPixels { buf: Buf { img, offset: [0.0; 2], scale: 1.0 }, size: [32.0, 32.0] })
|
||||
}
|
||||
fn layer_at(&self, id: u64, _: f64, me: bool) -> Option<crate::LayerPixels> {
|
||||
self.layer(id, me)
|
||||
}
|
||||
fn audio(&self, _: u64, _: f64, _: usize, _: u32) -> Option<Vec<f32>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn foam_flow_map_pop_velocity_wobble_textures_and_reflection() {
|
||||
let base = vec![
|
||||
("producer/producerPoint", pt(16.0, 16.0)),
|
||||
("producer/productionRate", num(2.0)),
|
||||
("physics/windSpeed", num(0.0)),
|
||||
("physics/turbulence", num(0.0)),
|
||||
("view", Value::Enum(2)),
|
||||
];
|
||||
let with = |extra: &[(&str, Value)], t: f64| {
|
||||
let mut v = base.clone();
|
||||
v.extend_from_slice(extra);
|
||||
run_fx("ec.sim.foam", &v, Image::new(32, 32), t, EffectEnv { host: Some(&FoamHost), ..Default::default() }).img
|
||||
};
|
||||
let cx = |img: &Image| {
|
||||
let (mut s, mut n) = (0.0, 0.0);
|
||||
for y in 0..32 {
|
||||
for x in 0..32 {
|
||||
let a = img.get(x, y)[3];
|
||||
s += a * x as f32;
|
||||
n += a;
|
||||
}
|
||||
}
|
||||
s / n.max(1e-6)
|
||||
};
|
||||
// Flow map: bubbles run downhill (towards the dark left side).
|
||||
let still = with(&[], 1.0);
|
||||
let flowing = with(&[("flowMap/flowMap", Value::Layer(Some(1))), ("flowMap/flowMapSteepness", num(1.0))], 1.0);
|
||||
assert!(cx(&flowing) < cx(&still) - 1.0, "{} vs {}", cx(&flowing), cx(&still));
|
||||
let hq =
|
||||
with(&[("flowMap/flowMap", Value::Layer(Some(1))), ("flowMap/flowMapSteepness", num(1.0)), ("flowMap/simulationQuality", Value::Enum(2))], 1.0);
|
||||
assert_eq!(
|
||||
hq,
|
||||
with(&[("flowMap/flowMap", Value::Layer(Some(1))), ("flowMap/flowMapSteepness", num(1.0)), ("flowMap/simulationQuality", Value::Enum(2))], 1.0)
|
||||
);
|
||||
// Pop velocity scatters the survivors when short-lived bubbles pop.
|
||||
let short = [("bubbles/lifespan", num(8.0)), ("bubbles/strength", num(0.0))];
|
||||
let a = with(&[short.as_slice(), &[("physics/popVelocity", num(0.0))]].concat(), 1.0);
|
||||
let b2 = with(&[short.as_slice(), &[("physics/popVelocity", num(20.0))]].concat(), 1.0);
|
||||
assert_ne!(a, b2);
|
||||
// Wobble changes the drawn bubbles.
|
||||
assert_ne!(with(&[("physics/wobbleAmount", num(0.0))], 1.0), with(&[("physics/wobbleAmount", num(3.0))], 1.0));
|
||||
// User Defined texture: bubbles show the texture layer (green).
|
||||
let tex = with(&[("rendering/bubbleTexture", Value::Enum(5)), ("rendering/bubbleTextureLayer", Value::Layer(Some(2)))], 1.0);
|
||||
assert!(tex.data.iter().any(|p| p[1] > 0.5 && p[0] < 0.1));
|
||||
// Environment map: reflections tint the bubbles.
|
||||
let refl = with(&[("rendering/environmentMap", Value::Layer(Some(2))), ("rendering/reflectionStrength", num(1.0))], 1.0);
|
||||
let g = |i: &Image| i.data.iter().map(|p| p[1] - p[0]).sum::<f32>();
|
||||
assert!(g(&refl) > g(&still) + 1.0);
|
||||
// Zoom Producer Point zooms about the producer instead of the layer centre.
|
||||
let off = [("producer/producerPoint", pt(8.0, 8.0)), ("zoom", num(2.0))];
|
||||
let z1 = with(&off, 1.0);
|
||||
let z2 = with(&[off.as_slice(), &[("producer/zoomProducerPoint", Value::Bool(true))]].concat(), 1.0);
|
||||
assert!((cx(&z2) - 8.0).abs() < 2.0 && cx(&z1) < 4.0, "{} vs {}", cx(&z2), cx(&z1));
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -3,11 +3,11 @@
|
||||
<!-- 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 303 effects with 2730 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 303 effects with 2742 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**: 24 effects also run on the GPU compositor with identical results.
|
||||
- **32**: 303 effects process 32-bit float (HDR, overbright) pixels without clamping.
|
||||
- **Status**: 274 implemented in full, 29 partial (what is missing is listed).
|
||||
- **Status**: 275 implemented in full, 28 partial (what is missing is listed).
|
||||
|
||||
Effects are addressed by id (`ec.<category>.<name>`) or display name in commands, scripts and the control channel, and every parameter by its id (`effects/#1/blurriness`).
|
||||
|
||||
@@ -330,7 +330,7 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
||||
| CC Star Burst | `ec.sim.ccstarburst` | 6 | | 32 | Implemented |
|
||||
| Card Dance | `ec.sim.carddance` | 53 | | 32 | Implemented |
|
||||
| Caustics | `ec.sim.caustics` | 21 | | 32 | Partial: no Sky group or light type |
|
||||
| Foam | `ec.sim.foam` | 24 | | 32 | Partial: no wobble, pop velocity, custom bubble texture, environment map or flow map |
|
||||
| Foam | `ec.sim.foam` | 36 | | 32 | Implemented |
|
||||
| Particle Playground | `ec.sim.particleplayground` | 92 | | 32 | Implemented |
|
||||
| Shatter | `ec.sim.shatter` | 60 | | 32 | Implemented |
|
||||
| Wave World | `ec.sim.waveworld` | 27 | | 32 | Partial: no wireframe controls, dry-area rendering or ground group |
|
||||
|
||||
Reference in new issue
Block a user