mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-30 04:57:54 -06:00
229 lines
10 KiB
Rust
229 lines
10 KiB
Rust
use super::pieces::{Cam, Piece, PiecePlan, PieceTex, rot_axis, sort_far_first};
|
|
use super::shatter::{ShatterPlan, shatter_impl};
|
|
use super::{Acc, Buf, EffectCtx, IntoParallelIterator, ParallelIterator, Post, Shape, Sprite, SpritePlan, h, hs, unpremul};
|
|
|
|
// ------------------------------------------------------------------ CC Ball Action
|
|
|
|
pub(super) fn ball_action(ctx: &EffectCtx, b: Buf) -> Buf {
|
|
ball_action_plan(ctx, &b).finish(b)
|
|
}
|
|
|
|
/// CC Ball Action's balls (far to near), coloured by the layer's pixels.
|
|
fn ball_action_plan(ctx: &EffectCtx, b: &Buf) -> SpritePlan {
|
|
let pr = ctx.params;
|
|
let scatter = pr.f("scatter") as f32;
|
|
let axis = pr.e("rotationAxis");
|
|
let rot = pr.f("rotation").to_radians();
|
|
let twist_prop = pr.e("twistProperty");
|
|
let twist = pr.f("twistAngle").to_radians();
|
|
let spacing = pr.f("gridSpacing").max(1.0) as f32;
|
|
let size = pr.f("ballSize") as f32 / 100.0;
|
|
let instab = pr.f("instabilityState").to_radians() as f32;
|
|
let seed = ctx.seed.wrapping_mul(0x2545f491);
|
|
let (lw, lh) = (ctx.layer_size[0] as f32, ctx.layer_size[1] as f32);
|
|
let s = b.scale as f32;
|
|
let nx = (lw / spacing).ceil().max(1.0) as u32;
|
|
let ny = (lh / spacing).ceil().max(1.0) as u32;
|
|
let d = 2.0 * lw.max(lh) as f64;
|
|
let (cx, cy) = (lw as f64 * 0.5, lh as f64 * 0.5);
|
|
let src = &b.img;
|
|
let ax: [f64; 3] = match axis {
|
|
0 => [1.0, 0.0, 0.0],
|
|
1 => [0.0, 1.0, 0.0],
|
|
2 => [0.0, 0.0, 1.0],
|
|
3 => [1.0, 1.0, 0.0],
|
|
4 => [1.0, 0.0, 1.0],
|
|
5 => [0.0, 1.0, 1.0],
|
|
_ => [1.0, 1.0, 1.0],
|
|
};
|
|
let maxr = ((lw * lw + lh * lh).sqrt() * 0.5).max(1.0);
|
|
let mut balls: Vec<(f64, Sprite)> = (0..nx * ny)
|
|
.into_par_iter()
|
|
.filter_map(|i| {
|
|
let gx = (i % nx) as f32 * spacing + spacing * 0.5;
|
|
let gy = (i / nx) as f32 * spacing + spacing * 0.5;
|
|
let (sx, sy) = b.to_px([gx as f64, gy as f64]);
|
|
let (c, a) = unpremul(src.sample_bilinear(sx, sy));
|
|
if a <= 0.0 || size <= 0.0 {
|
|
return None;
|
|
}
|
|
let tp = match twist_prop {
|
|
0 => gx / lw.max(1.0),
|
|
1 => gy / lh.max(1.0),
|
|
2 => ((gx - cx as f32).hypot(gy - cy as f32)) / maxr,
|
|
_ => crate::util::rgb3([c[0], c[1], c[2], 0.0]).iter().sum::<f32>() / 3.0,
|
|
} as f64;
|
|
let ang = rot + twist * tp;
|
|
// Scatter: random 3D offsets whose direction drifts with the instability state.
|
|
let ph = h(i, 1, seed) * std::f32::consts::TAU + instab;
|
|
let sc = scatter * h(i, 2, seed);
|
|
let off = [(sc * ph.cos()) as f64, (sc * ph.sin()) as f64, (scatter * hs(i, 3, seed)) as f64];
|
|
let p0 = [gx as f64 - cx + off[0], gy as f64 - cy + off[1], off[2]];
|
|
let m = rot_axis(ax, ang);
|
|
let q: [f64; 3] = std::array::from_fn(|k| m[k][0] * p0[0] + m[k][1] * p0[1] + m[k][2] * p0[2]);
|
|
if d + q[2] <= d * 0.05 {
|
|
return None;
|
|
}
|
|
let persp = d / (d + q[2]);
|
|
let (bx, by) = b.to_px([cx + q[0] * persp, cy + q[1] * persp]);
|
|
let r = spacing * 0.5 * size * persp as f32 * s;
|
|
Some((q[2], Sprite::new(bx as f32, by as f32, r, [c[0], c[1], c[2], a], Shape::Sphere)))
|
|
})
|
|
.collect();
|
|
balls.sort_by(|a, b| b.0.total_cmp(&a.0));
|
|
let sprites: Vec<Sprite> = balls.into_iter().map(|(_, s)| s).collect();
|
|
SpritePlan { sprites, tints: vec![], acc: Acc::Over, post: Post::Replace }
|
|
}
|
|
|
|
// ------------------------------------------------------------------ CC Pixel Polly
|
|
|
|
pub(super) fn pixel_polly(ctx: &EffectCtx, b: Buf) -> Buf {
|
|
match pixel_polly_plan(ctx, &b) {
|
|
Some(plan) => plan.finish(b),
|
|
None => b,
|
|
}
|
|
}
|
|
|
|
/// CC Pixel Polly's pieces, `None` before Start Time (the layer as is).
|
|
fn pixel_polly_plan(ctx: &EffectCtx, b: &Buf) -> Option<PiecePlan> {
|
|
let pr = ctx.params;
|
|
let t = ctx.time - pr.f("startTime");
|
|
if t <= 0.0 {
|
|
return None;
|
|
}
|
|
let force = pr.f("force");
|
|
let gravity = pr.f("gravity");
|
|
let spin = pr.f("spinning").to_radians();
|
|
let fc = pr.v2("forceCenter");
|
|
let dir_rand = pr.f("directionRandomness") / 100.0;
|
|
let spd_rand = pr.f("speedRandomness") / 100.0;
|
|
let g = pr.f("gridSpacing").max(1.0);
|
|
let object = pr.e("object");
|
|
let depth_sort = pr.b("enableDepthSort");
|
|
let seed = ctx.seed.wrapping_mul(0x632be5ab);
|
|
let [lw, lh] = ctx.layer_size;
|
|
let s = b.scale;
|
|
let cam = Cam { c: [b.offset[0] + lw * 0.5 * s, b.offset[1] + lh * 0.5 * s], d: 2.0 * lw.max(lh) * s };
|
|
let nx = (lw / g).ceil().max(1.0) as u32;
|
|
let ny = (lh / g).ceil().max(1.0) as u32;
|
|
let tri = object <= 1;
|
|
let textured = object == 1 || object == 3;
|
|
let maxd = lw.max(lh);
|
|
let src = &b.img;
|
|
let per = if tri { 2 } else { 1 };
|
|
let mut pieces: Vec<Piece> = (0..nx * ny * per)
|
|
.into_par_iter()
|
|
.filter_map(|i| {
|
|
let cell = i / per;
|
|
let (x0, y0) = ((cell % nx) as f64 * g, (cell / nx) as f64 * g);
|
|
let poly: Vec<[f64; 2]> = if !tri {
|
|
vec![[x0, y0], [x0 + g, y0], [x0 + g, y0 + g], [x0, y0 + g]]
|
|
} else if i % 2 == 0 {
|
|
vec![[x0, y0], [x0 + g, y0], [x0, y0 + g]]
|
|
} else {
|
|
vec![[x0 + g, y0], [x0 + g, y0 + g], [x0, y0 + g]]
|
|
};
|
|
let c = [poly.iter().map(|v| v[0]).sum::<f64>() / poly.len() as f64, poly.iter().map(|v| v[1]).sum::<f64>() / poly.len() as f64];
|
|
let (dx, dy) = (c[0] - fc[0], c[1] - fc[1]);
|
|
let dist = (dx * dx + dy * dy).sqrt().max(1e-6);
|
|
let ang = dy.atan2(dx) + dir_rand * std::f64::consts::PI * hs(i, 1, seed) as f64;
|
|
let speed = force / 100.0 * 0.6 * maxd / (1.0 + dist / (0.5 * maxd)) * (1.0 + spd_rand * hs(i, 2, seed) as f64);
|
|
let vz = -speed * 0.5 * h(i, 3, seed) as f64;
|
|
let pos = [c[0] + ang.cos() * speed * t, c[1] + ang.sin() * speed * t + 0.5 * gravity * 0.5 * lh * t * t, vz * t];
|
|
let axis = [hs(i, 4, seed) as f64, hs(i, 5, seed) as f64, hs(i, 6, seed) as f64];
|
|
let r = rot_axis(axis, t * (std::f64::consts::PI * h(i, 7, seed) as f64 + spin));
|
|
let tex: Vec<[f32; 2]> = poly.iter().map(|v| [(v[0] * s + b.offset[0]) as f32, (v[1] * s + b.offset[1]) as f32]).collect();
|
|
let c0 = [(c[0] * s + b.offset[0]) as f32, (c[1] * s + b.offset[1]) as f32];
|
|
let posb = [pos[0] * s + b.offset[0], pos[1] * s + b.offset[1], pos[2] * s];
|
|
let mut pc = Piece::new(&tex, c0, &r, posb, cam)?;
|
|
pc.shade = 0.35 + 0.65 * Piece::facing(&r);
|
|
if !textured {
|
|
let (cc, a) = unpremul(src.sample_bilinear(c0[0] as f64, c0[1] as f64));
|
|
if a <= 0.0 {
|
|
return None;
|
|
}
|
|
pc.flat = Some(cc);
|
|
pc.alpha = a;
|
|
}
|
|
Some(pc)
|
|
})
|
|
.collect();
|
|
if depth_sort {
|
|
sort_far_first(&mut pieces);
|
|
}
|
|
Some(PiecePlan { pieces, front: PieceTex::Layer, back: None })
|
|
}
|
|
|
|
// ------------------------------------------------------------------ CC Scatterize
|
|
|
|
pub(super) fn scatterize(ctx: &EffectCtx, b: Buf) -> Buf {
|
|
match scatterize_plan(ctx, &b) {
|
|
Some(plan) => plan.finish(b),
|
|
None => b,
|
|
}
|
|
}
|
|
|
|
/// CC Scatterize's pixel dots, `None` when nothing moves (the layer as is).
|
|
fn scatterize_plan(ctx: &EffectCtx, b: &Buf) -> Option<SpritePlan> {
|
|
let pr = ctx.params;
|
|
let scatter = pr.f("scatter") as f32;
|
|
let right = pr.f("rightTwist").to_radians();
|
|
let left = pr.f("leftTwist").to_radians();
|
|
if scatter == 0.0 && right == 0.0 && left == 0.0 {
|
|
return None;
|
|
}
|
|
let add = pr.e("transferMode") == 1;
|
|
let seed = ctx.seed.wrapping_mul(0x7feb352d);
|
|
let s = b.scale;
|
|
let (w, hh) = (b.img.width, b.img.height);
|
|
let (cx, cy) = (b.offset[0] + ctx.layer_size[0] * 0.5 * s, b.offset[1] + ctx.layer_size[1] * 0.5 * s);
|
|
let half = (ctx.layer_size[0] * 0.5 * s).max(1.0);
|
|
let d = 2.0 * ctx.layer_size[0].max(ctx.layer_size[1]) * s;
|
|
let src = &b.img;
|
|
let sprites: Vec<Sprite> = (0..w * hh)
|
|
.into_par_iter()
|
|
.filter_map(|i| {
|
|
let px = src.data[i as usize];
|
|
if px[3] <= 0.0 {
|
|
return None;
|
|
}
|
|
let (x, y) = ((i % w) as f64 + 0.5, (i / w) as f64 + 0.5);
|
|
// Twist about the horizontal axis through the centre, growing towards each side.
|
|
let k = ((x - cx) / half).abs().min(1.0);
|
|
let ang = if x < cx { left } else { right } * k;
|
|
let (sa, ca) = ang.sin_cos();
|
|
let (yy, zz) = ((y - cy) * ca, (y - cy) * sa);
|
|
let persp = d / (d + zz).max(d * 0.05);
|
|
let jx = hs(i, 1, seed) * scatter * s as f32;
|
|
let jy = hs(i, 2, seed) * scatter * s as f32;
|
|
let sx = cx + (x - cx) * persp + jx as f64;
|
|
let sy = cy + yy * persp + jy as f64;
|
|
let (c, a) = unpremul(px);
|
|
Some(Sprite::new(sx as f32, sy as f32, 0.6 * persp as f32, [c[0], c[1], c[2], a], Shape::Disc))
|
|
})
|
|
.collect();
|
|
Some(SpritePlan { sprites, tints: vec![], acc: if add { Acc::Add } else { Acc::Over }, post: Post::Replace })
|
|
}
|
|
|
|
/// What a particle effect that reads the layer's pixels draws this frame (CC Ball Action, CC
|
|
/// Pixel Polly, CC Scatterize, Shatter's wireframe views), for layer `b`; `None` for other
|
|
/// effects or a frame showing the layer as is.
|
|
pub enum PixelPlan {
|
|
Sprites(SpritePlan),
|
|
Pieces(PiecePlan),
|
|
}
|
|
|
|
/// The [`PixelPlan`] of effect `id` for layer `b` (its pixels are read).
|
|
pub fn pixel_plan(id: &str, ctx: &EffectCtx, b: &Buf) -> Option<PixelPlan> {
|
|
match id {
|
|
"ec.sim.ccballaction" => Some(PixelPlan::Sprites(ball_action_plan(ctx, b))),
|
|
"ec.sim.ccpixelpolly" => pixel_polly_plan(ctx, b).map(PixelPlan::Pieces),
|
|
"ec.sim.ccscatterize" => scatterize_plan(ctx, b).map(PixelPlan::Sprites),
|
|
"ec.sim.shatter" => Some(match shatter_impl(ctx, b) {
|
|
ShatterPlan::Pieces(plan) => PixelPlan::Pieces(plan),
|
|
ShatterPlan::Wire(plan) => PixelPlan::Sprites(plan),
|
|
}),
|
|
_ => None,
|
|
}
|
|
}
|