mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-28 20:16:52 -06:00
refactor(effects): split sim.rs into sim/*.rs
This commit is contained in:
+19
-1601
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,25 @@
|
||||
use super::*;
|
||||
|
||||
pub(crate) fn spec(id: &'static str, name: &'static str, params: Vec<crate::ParamSpec>, render: crate::RenderFn) -> EffectSpec {
|
||||
EffectSpec { id, name, category: "Simulation", params, render, gpu: false, float: true }
|
||||
}
|
||||
|
||||
pub(crate) fn pt(x: f64, y: f64) -> Value {
|
||||
Value::Vec2([x, y])
|
||||
}
|
||||
|
||||
pub(crate) fn pct() -> ParamUi {
|
||||
slider(0.0, 100.0, 0.0, 100.0, 1)
|
||||
}
|
||||
|
||||
/// Deterministic hash → [0, 1).
|
||||
#[inline]
|
||||
pub(crate) fn h(i: u32, k: u32, seed: u32) -> f32 {
|
||||
hash1(i, k, seed)
|
||||
}
|
||||
|
||||
/// Signed hash → [-1, 1).
|
||||
#[inline]
|
||||
pub(crate) fn hs(i: u32, k: u32, seed: u32) -> f32 {
|
||||
hash1(i, k, seed) * 2.0 - 1.0
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
use super::common::{h, hs};
|
||||
use super::particle_fx::{PS2_CACHE, PW_CACHE, ps2_phys, pw_phys};
|
||||
use super::sprites::{Shape, Sprite};
|
||||
use super::*;
|
||||
|
||||
// ------------------------------------------------------------------ particle engine
|
||||
|
||||
/// Steps per second of the stepped particle simulations.
|
||||
pub(crate) const SPS: f64 = 60.0;
|
||||
|
||||
/// Number of whole steps reached at layer time `t`.
|
||||
pub(crate) fn steps_at(t: f64) -> u64 {
|
||||
if t <= 0.0 { 0 } else { (t * SPS + 1e-6).floor() as u64 }
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct Particle {
|
||||
pub p: [f32; 3],
|
||||
pub v: [f32; 3],
|
||||
pub age: f32,
|
||||
pub life: f32,
|
||||
/// Per-particle random in [0, 1) (size variation, colour jitter…).
|
||||
pub rnd: f32,
|
||||
pub id: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub(crate) struct PState {
|
||||
pub parts: Vec<Particle>,
|
||||
pub carry: f64,
|
||||
pub next_id: u32,
|
||||
}
|
||||
|
||||
/// Physics configuration (world units; y down).
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(crate) struct Phys {
|
||||
/// Particles per second.
|
||||
pub rate: f64,
|
||||
pub life: f32,
|
||||
pub producer: [f32; 3],
|
||||
pub radius: [f32; 3],
|
||||
pub anim: u32,
|
||||
pub speed: f32,
|
||||
pub gravity: [f32; 3],
|
||||
pub resistance: f32,
|
||||
pub axis: [f32; 3],
|
||||
pub extra: f32,
|
||||
pub extra_angle: f32,
|
||||
pub seed: u32,
|
||||
pub max: usize,
|
||||
/// 2D systems keep z = 0.
|
||||
pub flat: bool,
|
||||
}
|
||||
|
||||
pub(crate) const PW_ANIMS: &[&str] =
|
||||
&["Explosive", "Direction Axis", "Cone Axis", "Viscouse", "Twirly", "Vortex", "Fire", "Jet Sideways", "Fractal Omni", "Fractal Uni"];
|
||||
|
||||
pub(super) fn norm3(v: [f32; 3]) -> [f32; 3] {
|
||||
let l = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
|
||||
if l > 1e-9 { [v[0] / l, v[1] / l, v[2] / l] } else { [0.0, -1.0, 0.0] }
|
||||
}
|
||||
|
||||
fn rand_dir(i: u32, k: u32, seed: u32, flat: bool) -> [f32; 3] {
|
||||
if flat {
|
||||
let a = h(i, k, seed) * std::f32::consts::TAU;
|
||||
return [a.cos(), a.sin(), 0.0];
|
||||
}
|
||||
let z = hs(i, k, seed);
|
||||
let a = h(i, k + 1, seed) * std::f32::consts::TAU;
|
||||
let r = (1.0 - z * z).max(0.0).sqrt();
|
||||
[r * a.cos(), r * a.sin(), z]
|
||||
}
|
||||
|
||||
impl Phys {
|
||||
fn spawn(&self, id: u32) -> Particle {
|
||||
let seed = self.seed;
|
||||
let mut p = [0.0f32; 3];
|
||||
// Uniform in the producer ellipsoid.
|
||||
let u = rand_dir(id, 1, seed, self.flat);
|
||||
let rr = h(id, 3, seed).powf(if self.flat { 0.5 } else { 1.0 / 3.0 });
|
||||
for k in 0..3 {
|
||||
p[k] = self.producer[k] + u[k] * self.radius[k] * rr;
|
||||
}
|
||||
if self.flat {
|
||||
p[2] = 0.0;
|
||||
}
|
||||
let ax = norm3(self.axis);
|
||||
let jitter = rand_dir(id, 5, seed, self.flat);
|
||||
let dir = match self.anim {
|
||||
1 | 9 => norm3([ax[0] + jitter[0] * 0.08, ax[1] + jitter[1] * 0.08, ax[2] + jitter[2] * 0.08]),
|
||||
2 => {
|
||||
let k = (self.extra_angle.to_radians() * 0.5).tan().abs().min(20.0);
|
||||
norm3([ax[0] + jitter[0] * k * h(id, 8, seed), ax[1] + jitter[1] * k * h(id, 8, seed), ax[2] + jitter[2] * k * h(id, 8, seed)])
|
||||
}
|
||||
5 => [jitter[0], 0.0, if self.flat { 0.0 } else { jitter[2] }],
|
||||
6 => norm3([jitter[0] * 0.25, -1.0, jitter[2] * 0.25]),
|
||||
7 => norm3([1.0, jitter[1] * 0.05, jitter[2] * 0.05]),
|
||||
_ => jitter,
|
||||
};
|
||||
let sp = self.speed * (0.5 + 0.5 * h(id, 9, seed));
|
||||
let mut v = [dir[0] * sp, dir[1] * sp, dir[2] * sp];
|
||||
if self.flat {
|
||||
v[2] = 0.0;
|
||||
}
|
||||
let life = self.life * (0.8 + 0.4 * h(id, 10, seed));
|
||||
Particle { p, v, age: 0.0, life, rnd: h(id, 11, seed), id }
|
||||
}
|
||||
|
||||
pub(crate) fn step(&self, st: &mut PState, step: u64) {
|
||||
let dt = (1.0 / SPS) as f32;
|
||||
let t = step as f32 * dt;
|
||||
let mut drag = self.resistance;
|
||||
if self.anim == 3 {
|
||||
drag += 3.0 * self.extra.max(0.1);
|
||||
}
|
||||
let damp = (-drag * dt).exp();
|
||||
let seed = self.seed;
|
||||
st.parts.par_iter_mut().for_each(|q| {
|
||||
let mut a = self.gravity;
|
||||
match self.anim {
|
||||
4 => {
|
||||
// Twirly: swirl around the vertical axis through the producer.
|
||||
let (dx, dz) = (q.p[0] - self.producer[0], q.p[2] - self.producer[2]);
|
||||
let k = 4.0 * self.extra;
|
||||
a[0] += -dz * k;
|
||||
a[2] += dx * k;
|
||||
if self.flat {
|
||||
let dy = q.p[1] - self.producer[1];
|
||||
a[0] += -dy * k;
|
||||
a[1] += dx * k;
|
||||
}
|
||||
}
|
||||
5 => {
|
||||
// Vortex: strong tangential + inward pull.
|
||||
let (dx, dy) = (q.p[0] - self.producer[0], q.p[1] - self.producer[1]);
|
||||
let k = 6.0 * self.extra;
|
||||
a[0] += -dy * k - dx * 2.0;
|
||||
a[1] += dx * k - dy * 2.0;
|
||||
}
|
||||
6 => {
|
||||
// Fire: buoyant, flickering turbulence.
|
||||
let n = value_noise(q.p[0] * 8.0, q.p[1] * 8.0, t * 3.0, seed) - 0.5;
|
||||
a[0] += n * 4.0 * self.extra;
|
||||
a[1] -= self.gravity[1] * 1.5 + 0.3 * self.extra;
|
||||
}
|
||||
8 | 9 => {
|
||||
let f = 6.0 * self.extra;
|
||||
a[0] += (value_noise(q.p[0] * 5.0, q.p[1] * 5.0, q.p[2] * 5.0 + t, seed) - 0.5) * f;
|
||||
a[1] += (value_noise(q.p[0] * 5.0 + 17.0, q.p[1] * 5.0, q.p[2] * 5.0 + t, seed) - 0.5) * f;
|
||||
if !self.flat {
|
||||
a[2] += (value_noise(q.p[0] * 5.0, q.p[1] * 5.0 + 31.0, q.p[2] * 5.0 + t, seed) - 0.5) * f;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
for k in 0..3 {
|
||||
q.v[k] = (q.v[k] + a[k] * dt) * damp;
|
||||
q.p[k] += q.v[k] * dt;
|
||||
}
|
||||
if self.flat {
|
||||
q.p[2] = 0.0;
|
||||
q.v[2] = 0.0;
|
||||
}
|
||||
q.age += dt;
|
||||
});
|
||||
st.parts.retain(|q| q.age < q.life);
|
||||
st.carry += self.rate / SPS;
|
||||
let n = st.carry.floor() as u32;
|
||||
st.carry -= n as f64;
|
||||
for _ in 0..n {
|
||||
if st.parts.len() >= self.max {
|
||||
break;
|
||||
}
|
||||
let id = st.next_id;
|
||||
st.next_id = st.next_id.wrapping_add(1);
|
||||
// Sub-step birth offset keeps continuous streams smooth.
|
||||
let mut q = self.spawn(id);
|
||||
let frac = h(id, 12, seed) * dt;
|
||||
for k in 0..3 {
|
||||
q.p[k] += q.v[k] * frac;
|
||||
}
|
||||
q.age = frac;
|
||||
st.parts.push(q);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Phys {
|
||||
/// The backend description (per-step and per-spawn constants evaluated here, in f32).
|
||||
pub(crate) fn desc(&self) -> crate::psim::EngineDesc {
|
||||
let mut drag = self.resistance;
|
||||
if self.anim == 3 {
|
||||
drag += 3.0 * self.extra.max(0.1);
|
||||
}
|
||||
let dt = (1.0 / SPS) as f32;
|
||||
crate::psim::EngineDesc {
|
||||
life: self.life,
|
||||
producer: self.producer,
|
||||
radius: self.radius,
|
||||
anim: self.anim,
|
||||
speed: self.speed,
|
||||
gravity: self.gravity,
|
||||
axis: norm3(self.axis),
|
||||
extra: self.extra,
|
||||
cone: (self.extra_angle.to_radians() * 0.5).tan().abs().min(20.0),
|
||||
damp: (-drag * dt).exp(),
|
||||
seed: self.seed,
|
||||
flat: self.flat,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The state at layer time `ctx.time` from the host's particle backend (GPU particles), or
|
||||
/// `None` (simulate on the CPU).
|
||||
pub(super) fn accelerated(ctx: &EffectCtx, key: u64, phys: &Phys) -> Option<std::sync::Arc<PState>> {
|
||||
let backend = ctx.env.host?.particles()?;
|
||||
let steps = steps_at(ctx.time);
|
||||
let births = crate::psim::births(phys.rate, steps, phys.max)?;
|
||||
let req = crate::psim::SimRequest { key, steps, births: &births, system: crate::psim::ParticleSystem::Engine(phys.desc()) };
|
||||
let parts = backend.simulate(&req)?;
|
||||
Some(std::sync::Arc::new(PState {
|
||||
parts: parts.into_iter().map(|q| Particle { p: q.p, v: q.v, age: q.age, life: q.life, rnd: q.rnd, id: q.id }).collect(),
|
||||
carry: 0.0,
|
||||
next_id: births.len() as u32,
|
||||
}))
|
||||
}
|
||||
|
||||
/// The live particles of a stepped particle effect (`ec.sim.ccparticleworld`,
|
||||
/// `ec.sim.ccparticlesystems2`) at `ctx.time`: from the host's particle backend when it has
|
||||
/// one (GPU particles), else from the CPU simulation. For comparing the two.
|
||||
pub fn particle_state(id: &str, ctx: &EffectCtx) -> Option<Vec<crate::psim::SimParticle>> {
|
||||
let (phys, cache, salt) = match id {
|
||||
"ec.sim.ccparticleworld" => (pw_phys(ctx), &PW_CACHE, 1),
|
||||
"ec.sim.ccparticlesystems2" => (ps2_phys(ctx), &PS2_CACHE, 2),
|
||||
_ => return None,
|
||||
};
|
||||
let key = sim_key(ctx, salt);
|
||||
let st = accelerated(ctx, key, &phys).unwrap_or_else(|| simulate(cache, key, ctx.time, &phys));
|
||||
Some(st.parts.iter().map(|q| crate::psim::SimParticle { p: q.p, v: q.v, age: q.age, life: q.life, rnd: q.rnd, id: q.id }).collect())
|
||||
}
|
||||
|
||||
/// The simulation cache key of a stepped particle effect: every parameter but CC Particle World's
|
||||
/// Effect Camera, which only changes how the particles are seen (animating it doesn't restart
|
||||
/// the simulation).
|
||||
pub(super) fn sim_key(ctx: &EffectCtx, salt: u64) -> u64 {
|
||||
params_key_except(ctx, &Buf { img: Image::new(0, 0), offset: [0.0; 2], scale: 1.0 }, salt, &[PW_EFFECT_CAMERA])
|
||||
}
|
||||
|
||||
/// Run a particle system to layer time `t` through `cache`.
|
||||
pub(crate) fn simulate(cache: &SimCache<PState>, key: u64, t: f64, phys: &Phys) -> std::sync::Arc<PState> {
|
||||
cache.run(key, steps_at(t), PState::default, |st, i| phys.step(st, i))
|
||||
}
|
||||
|
||||
pub(crate) const PARTICLE_TYPES: &[&str] =
|
||||
&["Line", "Shaded Sphere", "Faded Sphere", "Bubble", "Star", "Motion Polygon", "Tri-Polygon", "Cube", "Textured Square"];
|
||||
|
||||
/// Colour/size/opacity of a particle at its age.
|
||||
pub(super) fn particle_look(q: &Particle, birth: [f32; 4], death: [f32; 4], bsize: f32, dsize: f32, svar: f32, max_op: f32) -> ([f32; 4], f32) {
|
||||
let u = (q.age / q.life.max(1e-4)).clamp(0.0, 1.0);
|
||||
let size = (bsize + (dsize - bsize) * u) * (1.0 + svar * (q.rnd * 2.0 - 1.0));
|
||||
let fade = (u * 20.0).min(1.0) * ((1.0 - u) * 5.0).min(1.0);
|
||||
let c = [birth[0] + (death[0] - birth[0]) * u, birth[1] + (death[1] - birth[1]) * u, birth[2] + (death[2] - birth[2]) * u, max_op * fade];
|
||||
(c, size.max(0.0))
|
||||
}
|
||||
|
||||
pub(super) fn type_sprite(kind: u32, x: f32, y: f32, r: f32, c: [f32; 4], vx: f32, vy: f32, rot: f32) -> Sprite {
|
||||
let vl = (vx * vx + vy * vy).sqrt().max(1e-6);
|
||||
let shape = match kind {
|
||||
0 => Shape::Line { dx: -vx / vl * r * 2.0, dy: -vy / vl * r * 2.0 },
|
||||
1 => Shape::Sphere,
|
||||
2 => Shape::Faded,
|
||||
3 => Shape::Bubble,
|
||||
4 => Shape::Star,
|
||||
5 => Shape::Line { dx: -vx * 0.05, dy: -vy * 0.05 },
|
||||
6 => Shape::Tri,
|
||||
_ => Shape::Square,
|
||||
};
|
||||
let rr = match kind {
|
||||
0 => (r * 0.08).max(0.5),
|
||||
5 => r * 0.5,
|
||||
_ => r,
|
||||
};
|
||||
Sprite { x, y, r: rr, c, shape, rot }
|
||||
}
|
||||
|
||||
/// CC Particle World's Extras ▸ Effect Camera parameters (id prefix).
|
||||
pub(super) const PW_EFFECT_CAMERA: &str = "extras/effectCamera/";
|
||||
@@ -0,0 +1,367 @@
|
||||
use super::engine::{PState, PW_EFFECT_CAMERA, Phys, accelerated, norm3, particle_look, sim_key, simulate, type_sprite};
|
||||
use super::sprites::{Acc, Post, Sprite, SpritePlan, raster, splat};
|
||||
use super::*;
|
||||
|
||||
// ------------------------------------------------------------------ CC Particle World
|
||||
|
||||
pub(super) static PW_CACHE: SimCache<PState> = SimCache::new(6);
|
||||
|
||||
/// CC Particle World's physics.
|
||||
pub(super) fn pw_phys(ctx: &EffectCtx) -> Phys {
|
||||
let pr = ctx.params;
|
||||
Phys {
|
||||
rate: pr.f("birthRate").max(0.0) * 120.0,
|
||||
life: pr.f("longevity").max(0.0) as f32,
|
||||
producer: [pr.f("producer/producerX") as f32, pr.f("producer/producerY") as f32, pr.f("producer/producerZ") as f32],
|
||||
radius: [pr.f("producer/radiusX").max(0.0) as f32, pr.f("producer/radiusY").max(0.0) as f32, pr.f("producer/radiusZ").max(0.0) as f32],
|
||||
anim: pr.e("physics/animation"),
|
||||
speed: pr.f("physics/velocity") as f32 * 0.5,
|
||||
gravity: {
|
||||
let g = pr.f("physics/gravity") as f32;
|
||||
let gv =
|
||||
[pr.f("physics/gravityVector/gravityX") as f32, pr.f("physics/gravityVector/gravityY") as f32, pr.f("physics/gravityVector/gravityZ") as f32];
|
||||
[gv[0] * g, gv[1] * g, gv[2] * g]
|
||||
},
|
||||
resistance: pr.f("physics/resistance").max(0.0) as f32,
|
||||
axis: [pr.f("physics/directionAxis/axisX") as f32, pr.f("physics/directionAxis/axisY") as f32, pr.f("physics/directionAxis/axisZ") as f32],
|
||||
extra: pr.f("physics/extra") as f32,
|
||||
extra_angle: pr.f("physics/extraAngle") as f32,
|
||||
seed: (pr.f("extras/randomSeed") as u32).wrapping_mul(0x9e3779b9) ^ ctx.seed,
|
||||
max: 40_000,
|
||||
flat: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn particle_world(ctx: &EffectCtx, b: Buf) -> Buf {
|
||||
// #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 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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
acc.data.iter_mut().for_each(|p| p.iter_mut().for_each(|v| *v /= n as f32));
|
||||
return match post {
|
||||
Some(p) => p.apply(b, acc),
|
||||
None => b,
|
||||
};
|
||||
}
|
||||
match particle_world_plan(ctx, &b) {
|
||||
Some(plan) => plan.finish(b),
|
||||
None => b,
|
||||
}
|
||||
}
|
||||
|
||||
/// Effect Camera ▸ FOV's default (degrees).
|
||||
pub(super) const PW_FOV: f64 = 50.0;
|
||||
|
||||
/// What CC Particle World sees its particles through.
|
||||
enum PwCamera {
|
||||
/// Extras ▸ Effect Camera, in world units (layer widths): `dist` from the world origin (the
|
||||
/// layer centre) along −z, the world turned by `rot` (`None` = not turned) about the origin,
|
||||
/// `focal` the scale at unit depth; particles nearer than `near` aren't drawn.
|
||||
Effect { rot: Option<[[f32; 3]; 3]>, dist: f32, focal: f32, near: f32 },
|
||||
/// The comp's active camera layer, on buffer-world points (After Effects then ignores the
|
||||
/// Effect Camera).
|
||||
Comp(crate::card3d::Proj),
|
||||
}
|
||||
|
||||
/// A particle as the camera sees it: depth (larger is farther), buffer pixel, size scale `k`
|
||||
/// (buffer pixels per world unit = `k · layer width · buffer scale`) and velocity in world units
|
||||
/// on screen (× the same factor for buffer pixels).
|
||||
struct PwSeen {
|
||||
depth: f32,
|
||||
x: f32,
|
||||
y: f32,
|
||||
k: f32,
|
||||
v: [f32; 2],
|
||||
}
|
||||
|
||||
impl PwCamera {
|
||||
fn new(ctx: &EffectCtx, b: &Buf) -> PwCamera {
|
||||
let [lw, lh] = ctx.layer_size;
|
||||
if let Some(cam) = ctx.env.host.and_then(|h| h.comp_scene()).filter(|sc| sc.camera_layer).and_then(|sc| sc.camera) {
|
||||
let c = [b.offset[0] + lw * 0.5 * b.scale, b.offset[1] + lh * 0.5 * b.scale];
|
||||
return PwCamera::Comp(crate::card3d::comp_proj(&cam, c, b));
|
||||
}
|
||||
let f = |id: &str, d: f64| ctx.params.get(&format!("{PW_EFFECT_CAMERA}{id}")).map(Value::as_f64).filter(|v| v.is_finite()).unwrap_or(d);
|
||||
// The camera keeps the origin's plane at the scale Distance gives it as FOV changes
|
||||
// (moving in for a wider view), so FOV sets how strong the perspective is. Distance 2
|
||||
// shows the origin's plane at the layer's own scale.
|
||||
let half = |fov: f64| (fov.clamp(1.0, 179.0).to_radians() * 0.5).tan();
|
||||
let zoom = half(PW_FOV) / half(f("fov", PW_FOV));
|
||||
let dist = f("distance", 2.0).max(0.01) * zoom;
|
||||
let a = [f("rotationX", 0.0), f("rotationY", 0.0), f("rotationZ", 0.0)];
|
||||
let rot = (a != [0.0; 3]).then(|| {
|
||||
let r = crate::card3d::rot_ordered(a[0].to_radians(), a[1].to_radians(), a[2].to_radians(), 0);
|
||||
r.map(|row| row.map(|v| v as f32))
|
||||
});
|
||||
PwCamera::Effect { rot, dist: dist as f32, focal: (2.0 * zoom) as f32, near: (0.025 * dist) as f32 }
|
||||
}
|
||||
|
||||
/// How the camera sees world point `p` moving at `v`; `None` behind or too close to it.
|
||||
fn see(&self, p: [f32; 3], v: [f32; 3], lw: f32, lh: f32, b: &Buf) -> Option<PwSeen> {
|
||||
match self {
|
||||
PwCamera::Effect { rot, dist, focal, near } => {
|
||||
let turn = |r: &[[f32; 3]; 3], q: [f32; 3]| r.map(|row| row[0] * q[0] + row[1] * q[1] + row[2] * q[2]);
|
||||
let (p, v) = match rot {
|
||||
Some(r) => (turn(r, p), turn(r, v)),
|
||||
None => (p, v),
|
||||
};
|
||||
let zc = dist + p[2];
|
||||
if zc.is_nan() || zc <= *near {
|
||||
return None;
|
||||
}
|
||||
let k = focal / zc;
|
||||
let (x, y) = b.to_px([(lw * 0.5 + p[0] * lw * k) as f64, (lh * 0.5 + p[1] * lw * k) as f64]);
|
||||
Some(PwSeen { depth: zc, x: x as f32, y: y as f32, k, v: [v[0], v[1]] })
|
||||
}
|
||||
PwCamera::Comp(proj) => {
|
||||
let (s, lw, lh) = (b.scale, lw as f64, lh as f64);
|
||||
let u = lw * s;
|
||||
let at = [b.offset[0] + lw * 0.5 * s + p[0] as f64 * u, b.offset[1] + lh * 0.5 * s + p[1] as f64 * u, p[2] as f64 * u];
|
||||
let seen = proj.view(at, [v[0] as f64, v[1] as f64, v[2] as f64])?;
|
||||
let k = seen.scale;
|
||||
Some(PwSeen {
|
||||
depth: seen.depth as f32,
|
||||
x: seen.at[0] as f32,
|
||||
y: seen.at[1] as f32,
|
||||
k: k as f32,
|
||||
v: [(seen.vel[0] / k) as f32, (seen.vel[1] / k) as f32],
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// CC Particle World's sprites (far to near), `None` before anything is born.
|
||||
pub(super) fn particle_world_plan(ctx: &EffectCtx, b: &Buf) -> Option<SpritePlan> {
|
||||
let pr = ctx.params;
|
||||
let (lw, lh) = (ctx.layer_size[0] as f32, ctx.layer_size[1] as f32);
|
||||
let phys = pw_phys(ctx);
|
||||
if phys.rate <= 0.0 && ctx.time <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
let key = sim_key(ctx, 1);
|
||||
let st = accelerated(ctx, key, &phys).unwrap_or_else(|| simulate(&PW_CACHE, key, ctx.time, &phys));
|
||||
let kind = pr.e("particle/particleType");
|
||||
let birth = pr.color("particle/birthColor");
|
||||
let death = pr.color("particle/deathColor");
|
||||
let (bs, ds) = (pr.f("particle/birthSize") as f32, pr.f("particle/deathSize") as f32);
|
||||
let svar = pr.f("particle/sizeVariation") as f32 / 100.0;
|
||||
let max_op = pr.f("particle/maxOpacity") as f32 / 100.0;
|
||||
let s = b.scale as f32;
|
||||
let cam = PwCamera::new(ctx, b);
|
||||
let mut list: Vec<(f32, Sprite)> = st
|
||||
.parts
|
||||
.par_iter()
|
||||
.filter_map(|q| {
|
||||
let PwSeen { depth, x, y, k, v } = cam.see(q.p, q.v, lw, lh, b)?;
|
||||
let (c, size) = particle_look(q, birth, death, bs, ds, svar, max_op);
|
||||
let r = size * 0.5 * lw * k * s * 0.25;
|
||||
let sp = type_sprite(kind, x, y, r, c, v[0] * lw * k * s, v[1] * lw * k * s, (q.id % 628) as f32 * 0.01 + q.age * 2.0);
|
||||
Some((depth, sp))
|
||||
})
|
||||
.collect();
|
||||
list.sort_by(|a, b| b.0.total_cmp(&a.0));
|
||||
let sprites: Vec<Sprite> = list.into_iter().map(|(_, s)| s).collect();
|
||||
Some(particle_sprites(sprites, pr.e("particle/transferMode")))
|
||||
}
|
||||
|
||||
/// The CC particle systems' sprites with their Transfer Mode (Composite, Screen, Add, Black
|
||||
/// Background).
|
||||
fn particle_sprites(sprites: Vec<Sprite>, mode: u32) -> SpritePlan {
|
||||
// The particles replace the layer (After Effects shows no trace of the source in any
|
||||
// Transfer Mode, which sets how the particles meet each other).
|
||||
SpritePlan { sprites, tints: vec![], acc: if mode == 2 { Acc::Add } else { Acc::Over }, post: Post::Replace }
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ CC Particle Systems II
|
||||
|
||||
pub(super) static PS2_CACHE: SimCache<PState> = SimCache::new(6);
|
||||
|
||||
/// CC Particle Systems II's physics.
|
||||
pub(super) fn ps2_phys(ctx: &EffectCtx) -> Phys {
|
||||
let pr = ctx.params;
|
||||
let lh = ctx.layer_size[1] as f32;
|
||||
let unit = lh.max(1.0);
|
||||
let pos = pr.v2("producer/position");
|
||||
let dir = (pr.f("physics/direction") as f32).to_radians();
|
||||
Phys {
|
||||
rate: pr.f("birthRate").max(0.0) * 60.0,
|
||||
life: pr.f("longevity").max(0.0) as f32,
|
||||
producer: [pos[0] as f32 / unit, pos[1] as f32 / unit, 0.0],
|
||||
radius: [pr.f("producer/radiusX").max(0.0) as f32 / unit, pr.f("producer/radiusY").max(0.0) as f32 / unit, 0.0],
|
||||
anim: [0u32, 1, 2, 3, 4, 5, 6, 7, 8, 9][pr.e("physics/animation").min(9) as usize],
|
||||
speed: pr.f("physics/velocity") as f32 * 0.25,
|
||||
gravity: [0.0, pr.f("physics/gravity") as f32 * 0.5, 0.0],
|
||||
resistance: pr.f("physics/resistance").max(0.0) as f32,
|
||||
// AE angles: 0° = up, clockwise.
|
||||
axis: [dir.sin(), -dir.cos(), 0.0],
|
||||
extra: pr.f("physics/extra") as f32,
|
||||
extra_angle: 45.0 * pr.f("physics/extra") as f32,
|
||||
seed: (pr.f("randomSeed") as u32).wrapping_mul(0x85ebca6b) ^ ctx.seed,
|
||||
max: 40_000,
|
||||
flat: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn particle_systems2(ctx: &EffectCtx, b: Buf) -> Buf {
|
||||
particle_systems2_plan(ctx, &b).finish(b)
|
||||
}
|
||||
|
||||
/// CC Particle Systems II's sprites.
|
||||
pub(super) fn particle_systems2_plan(ctx: &EffectCtx, b: &Buf) -> SpritePlan {
|
||||
let pr = ctx.params;
|
||||
let lh = ctx.layer_size[1] as f32;
|
||||
let unit = lh.max(1.0);
|
||||
let phys = ps2_phys(ctx);
|
||||
let key = sim_key(ctx, 2);
|
||||
let st = accelerated(ctx, key, &phys).unwrap_or_else(|| simulate(&PS2_CACHE, key, ctx.time, &phys));
|
||||
let kind = pr.e("particle/particleType");
|
||||
let birth = pr.color("particle/birthColor");
|
||||
let death = pr.color("particle/deathColor");
|
||||
let (bs, ds) = (pr.f("particle/birthSize") as f32, pr.f("particle/deathSize") as f32);
|
||||
let svar = pr.f("particle/sizeVariation") as f32 / 100.0;
|
||||
let max_op = pr.f("particle/maxOpacity") as f32 / 100.0;
|
||||
let s = b.scale as f32;
|
||||
let sprites: Vec<Sprite> = st
|
||||
.parts
|
||||
.iter()
|
||||
.map(|q| {
|
||||
let (bx, by) = b.to_px([(q.p[0] * unit) as f64, (q.p[1] * unit) as f64]);
|
||||
let (c, size) = particle_look(q, birth, death, bs, ds, svar, max_op);
|
||||
let r = size * unit * 0.02 * s;
|
||||
type_sprite(kind, bx as f32, by as f32, r, c, q.v[0] * unit * s, q.v[1] * unit * s, (q.id % 628) as f32 * 0.01 + q.age * 2.0)
|
||||
})
|
||||
.collect();
|
||||
particle_sprites(sprites, pr.e("particle/transferMode"))
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ CC Mr. Mercury
|
||||
|
||||
static MERCURY_CACHE: SimCache<PState> = SimCache::new(6);
|
||||
|
||||
/// A CC Mr. Mercury blob (buffer px).
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Blob {
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
pub r: f32,
|
||||
}
|
||||
|
||||
/// CC Mr. Mercury's blobs this frame, from its particle simulation (for buffer geometry `b`).
|
||||
pub fn mercury_blobs(ctx: &EffectCtx, b: &Buf) -> Vec<Blob> {
|
||||
let pr = ctx.params;
|
||||
let lh = ctx.layer_size[1] as f32;
|
||||
let unit = lh.max(1.0);
|
||||
let pos = pr.v2("producer");
|
||||
let dir = (pr.f("direction") as f32).to_radians();
|
||||
let phys = Phys {
|
||||
rate: pr.f("birthRate").max(0.0) * 10.0,
|
||||
life: pr.f("longevity").max(0.0) as f32,
|
||||
producer: [pos[0] as f32 / unit, pos[1] as f32 / unit, 0.0],
|
||||
radius: [pr.f("radiusX").max(0.0) as f32 / unit, pr.f("radiusY").max(0.0) as f32 / unit, 0.0],
|
||||
anim: [0u32, 1, 2, 3, 4, 5, 6, 7, 8, 9][pr.e("animation").min(9) as usize],
|
||||
speed: pr.f("velocity") as f32 * 0.3,
|
||||
gravity: [0.0, pr.f("gravity") as f32 * 0.5, 0.0],
|
||||
resistance: pr.f("resistance").max(0.0) as f32,
|
||||
axis: [dir.sin(), -dir.cos(), 0.0],
|
||||
extra: pr.f("extra") as f32,
|
||||
extra_angle: 45.0 * pr.f("extra") as f32,
|
||||
seed: ctx.seed.wrapping_mul(0xc2b2ae35),
|
||||
max: 4_000,
|
||||
flat: true,
|
||||
};
|
||||
let st = simulate(&MERCURY_CACHE, params_key(ctx, &Buf { img: Image::new(0, 0), offset: [0.0; 2], scale: 1.0 }, 3), ctx.time, &phys);
|
||||
let (bs, ds) = (pr.f("blobBirthSize") as f32, pr.f("blobDeathSize") as f32);
|
||||
let s = b.scale as f32;
|
||||
st.parts
|
||||
.iter()
|
||||
.map(|q| {
|
||||
let u = (q.age / q.life.max(1e-4)).clamp(0.0, 1.0);
|
||||
let size = bs + (ds - bs) * u;
|
||||
let (bx, by) = b.to_px([(q.p[0] * unit) as f64, (q.p[1] * unit) as f64]);
|
||||
Blob { x: bx as f32, y: by as f32, r: (size * unit * 0.08 * s).max(0.0) }
|
||||
})
|
||||
.filter(|q| q.r > 0.1)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(super) fn mr_mercury(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let pr = ctx.params;
|
||||
let blobs = mercury_blobs(ctx, &b);
|
||||
let influence = pr.f("blobInfluence") as f32 / 100.0;
|
||||
let s = b.scale as f32;
|
||||
let (w, hh) = (b.img.width, b.img.height);
|
||||
// Field and gradient accumulate additively: [f, gx, gy, _].
|
||||
let mut field = Image::new(w, hh);
|
||||
let infl = 1.0 + influence;
|
||||
raster(
|
||||
&mut field,
|
||||
&blobs,
|
||||
|q| {
|
||||
let e = q.r * 2.0 * infl;
|
||||
Some([q.x - e, q.y - e, q.x + e, q.y + e])
|
||||
},
|
||||
|q, x, y| {
|
||||
let (dx, dy) = (x - q.x, y - q.y);
|
||||
let rr = q.r * 2.0 * infl;
|
||||
let d2 = (dx * dx + dy * dy) / (rr * rr);
|
||||
if d2 >= 1.0 {
|
||||
return None;
|
||||
}
|
||||
let k = 1.0 - d2;
|
||||
let f = k * k * k;
|
||||
let g = -6.0 * k * k / (rr * rr);
|
||||
Some([f, g * dx, g * dy, 0.0])
|
||||
},
|
||||
Acc::Add,
|
||||
);
|
||||
let thr = 0.35f32;
|
||||
let ambient = pr.f("shading/ambient") as f32 / 100.0;
|
||||
let diffuse = pr.f("shading/diffuse") as f32 / 100.0;
|
||||
let specular = pr.f("shading/specular") as f32 / 100.0;
|
||||
let rough = pr.f("shading/roughness").max(0.001) as f32;
|
||||
let metal = pr.f("shading/metal") as f32 / 100.0;
|
||||
let li = pr.f("light/lightIntensity") as f32 / 100.0;
|
||||
let lc = pr.color("light/lightColor");
|
||||
let ldir = (pr.f("light/lightDirection") as f32).to_radians();
|
||||
let lheight = pr.f("light/lightHeight") as f32 / 100.0;
|
||||
let l = norm3([ldir.sin(), -ldir.cos(), lheight.max(0.05)]);
|
||||
let src = b.img.clone();
|
||||
let mut out = Image::new(w, hh);
|
||||
out.rows_mut().for_each(|(y, row)| {
|
||||
for (x, px) in row.iter_mut().enumerate() {
|
||||
let fv = field.data[y * w as usize + x];
|
||||
let f = fv[0];
|
||||
if f <= thr * 0.8 {
|
||||
continue;
|
||||
}
|
||||
let cov = ((f - thr * 0.8) / (thr * 0.4)).clamp(0.0, 1.0);
|
||||
let n = norm3([fv[1] * 3.0, fv[2] * 3.0, 1.0 / (1.0 + f)]);
|
||||
// Refract the layer through the blob.
|
||||
let sp = src.sample_bilinear_clamped(x as f64 + 0.5 - n[0] as f64 * 12.0 * s as f64, y as f64 + 0.5 - n[1] as f64 * 12.0 * s as f64);
|
||||
let (c, sa) = unpremul(sp);
|
||||
let base = if sa > 0.0 { c } else { [0.7, 0.7, 0.75] };
|
||||
let nl = (n[0] * l[0] + n[1] * l[1] + n[2] * l[2]).max(0.0);
|
||||
let hv = norm3([l[0], l[1], l[2] + 1.0]);
|
||||
let spec = (n[0] * hv[0] + n[1] * hv[1] + n[2] * hv[2]).max(0.0).powf(1.0 / rough) * specular * li;
|
||||
let shade = ambient + diffuse * nl * li;
|
||||
let cc: [f32; 3] = std::array::from_fn(|k| base[k] * shade + spec * (lc[k] * (1.0 - metal) + base[k] * metal));
|
||||
*px = [cc[0] * cov, cc[1] * cov, cc[2] * cov, cov];
|
||||
}
|
||||
});
|
||||
b.img = out;
|
||||
b
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
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::sprites::SpritePlan;
|
||||
use super::weather::{bubbles, drizzle, rainfall, rainfall_plan, snowfall, snowfall_plan, star_burst, star_burst_plan};
|
||||
use super::*;
|
||||
|
||||
/// The sprite plan of a sprite-drawing simulation effect for buffer geometry `b` (its pixels
|
||||
/// are not read), `None` for other effects or settings drawn otherwise (the GPU compositor's
|
||||
/// render pass).
|
||||
pub fn sprite_plan(id: &str, ctx: &EffectCtx, b: &Buf) -> Option<SpritePlan> {
|
||||
match id {
|
||||
"ec.sim.ccrainfall" => Some(rainfall_plan(ctx, b)),
|
||||
"ec.sim.ccsnowfall" => Some(snowfall_plan(ctx, b)),
|
||||
"ec.sim.ccstarburst" => Some(star_burst_plan(ctx, b)),
|
||||
"ec.sim.cchair" => crate::sim3::hair_plan(ctx, b),
|
||||
"ec.sim.waveworld" => crate::sim2::wave_world_plan(ctx, b).and_then(|p| match p {
|
||||
crate::sim2::WavePlan::Wire(sp) => Some(sp),
|
||||
crate::sim2::WavePlan::Height { .. } => None,
|
||||
}),
|
||||
"ec.sim.foam" => crate::sim2::foam_plan(ctx, b),
|
||||
"ec.sim.ccparticleworld" => particle_world_plan(ctx, b),
|
||||
"ec.sim.ccparticlesystems2" => Some(particle_systems2_plan(ctx, b)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ specs
|
||||
|
||||
pub fn specs() -> Vec<EffectSpec> {
|
||||
let transfer = || popup(&["Composite", "Lighten"]);
|
||||
vec![
|
||||
spec(
|
||||
"ec.sim.ccrainfall",
|
||||
"CC Rainfall",
|
||||
vec![
|
||||
p("drops", "Drops", num(5000.0), slider(0.0, 100_000.0, 0.0, 20_000.0, 0)),
|
||||
p("size", "Size", num(1.0), slider(0.0, 50.0, 0.0, 10.0, 2)),
|
||||
p("sceneDepth", "Scene Depth", num(5000.0), slider(0.0, 20_000.0, 0.0, 10_000.0, 0)),
|
||||
p("speed", "Speed", num(4000.0), slider(0.0, 20_000.0, 0.0, 10_000.0, 0)),
|
||||
p("wind", "Wind", num(0.0), slider(-10_000.0, 10_000.0, -2000.0, 2000.0, 1)),
|
||||
p("windVariation", "Variation % (Wind)", num(0.0), pct()),
|
||||
p("spread", "Spread", num(0.0), slider(0.0, 1000.0, 0.0, 100.0, 1)),
|
||||
p("color", "Color", col(0.8, 0.8, 0.8), ParamUi::Color),
|
||||
p("opacity", "Opacity", num(25.0), pct()),
|
||||
p("transferMode", "Transfer Mode", Value::Enum(1), transfer()),
|
||||
p("compositeWithOriginal", "Composite With Original", Value::Bool(true), ParamUi::Checkbox),
|
||||
p("extras/appearance", "Appearance", Value::Enum(0), popup(&["Refracting", "Soft Solid"])),
|
||||
p("extras/offset", "Offset", pt(0.5, 0.5), ParamUi::Point),
|
||||
p("extras/randomSeed", "Random Seed", num(0.0), slider(0.0, 10_000.0, 0.0, 1000.0, 0)),
|
||||
],
|
||||
rainfall,
|
||||
),
|
||||
spec(
|
||||
"ec.sim.ccsnowfall",
|
||||
"CC Snowfall",
|
||||
vec![
|
||||
p("flakes", "Flakes", num(10_000.0), slider(0.0, 200_000.0, 0.0, 50_000.0, 0)),
|
||||
p("size", "Size", num(3.0), slider(0.0, 50.0, 0.0, 20.0, 2)),
|
||||
p("sizeVariation", "Variation % (Size)", num(50.0), pct()),
|
||||
p("sceneDepth", "Scene Depth", num(5000.0), slider(0.0, 20_000.0, 0.0, 10_000.0, 0)),
|
||||
p("speed", "Speed", num(100.0), slider(0.0, 5000.0, 0.0, 1000.0, 1)),
|
||||
p("speedVariation", "Variation % (Speed)", num(50.0), pct()),
|
||||
p("wind", "Wind", num(25.0), slider(-5000.0, 5000.0, -500.0, 500.0, 1)),
|
||||
p("windVariation", "Variation % (Wind)", num(50.0), pct()),
|
||||
p("spread", "Spread", num(0.0), slider(0.0, 1000.0, 0.0, 100.0, 1)),
|
||||
p("wiggle/wiggleAmount", "Amount", num(2.0), slider(0.0, 100.0, 0.0, 20.0, 2)),
|
||||
p("wiggle/wiggleAmountVariation", "Variation % (Amount)", num(50.0), pct()),
|
||||
p("wiggle/wiggleFrequency", "Frequency", num(1.0), slider(0.0, 20.0, 0.0, 5.0, 2)),
|
||||
p("wiggle/wiggleFrequencyVariation", "Variation % (Frequency)", num(50.0), pct()),
|
||||
p("color", "Color", col(1.0, 1.0, 1.0), ParamUi::Color),
|
||||
p("opacity", "Opacity", num(100.0), pct()),
|
||||
p("transferMode", "Transfer Mode", Value::Enum(0), transfer()),
|
||||
p("compositeWithOriginal", "Composite With Original", Value::Bool(true), ParamUi::Checkbox),
|
||||
p("extras/offset", "Offset", pt(0.5, 0.5), ParamUi::Point),
|
||||
p("extras/randomSeed", "Random Seed", num(0.0), slider(0.0, 10_000.0, 0.0, 1000.0, 0)),
|
||||
],
|
||||
snowfall,
|
||||
),
|
||||
spec(
|
||||
"ec.sim.ccbubbles",
|
||||
"CC Bubbles",
|
||||
vec![
|
||||
p("bubbleAmount", "Bubble Amount", num(100.0), slider(0.0, 10_000.0, 0.0, 1000.0, 0)),
|
||||
p("bubbleSpeed", "Bubble Speed", num(0.5), slider(-10.0, 10.0, -2.0, 2.0, 2)),
|
||||
p("wobbleAmplitude", "Wobble Amplitude", num(5.0), slider(0.0, 500.0, 0.0, 100.0, 1)),
|
||||
p("wobbleFrequency", "Wobble Frequency", num(1.0), slider(0.0, 50.0, 0.0, 10.0, 2)),
|
||||
p("bubbleSize", "Bubble Size", num(1.0), slider(0.0, 20.0, 0.0, 5.0, 2)),
|
||||
p("reflectionType", "Reflection Type", Value::Enum(0), popup(&["Liquid", "Metal"])),
|
||||
p("shadingType", "Shading Type", Value::Enum(0), popup(&["None", "Lighten", "Darken", "Fade Inwards", "Fade Outwards"])),
|
||||
],
|
||||
bubbles,
|
||||
),
|
||||
spec(
|
||||
"ec.sim.ccdrizzle",
|
||||
"CC Drizzle",
|
||||
vec![
|
||||
p("dripRate", "Drip Rate", num(10.0), slider(0.0, 200.0, 0.0, 50.0, 2)),
|
||||
p("longevity", "Longevity (sec)", num(1.0), slider(0.0, 30.0, 0.0, 5.0, 2)),
|
||||
p("rippling", "Rippling", num(360.0), ParamUi::Angle),
|
||||
p("displacement", "Displacement", num(5.0), slider(0.0, 100.0, 0.0, 30.0, 2)),
|
||||
p("rippleHeight", "Ripple Height", num(30.0), slider(0.0, 200.0, 0.0, 100.0, 1)),
|
||||
p("spreading", "Spreading", num(100.0), slider(0.0, 1000.0, 0.0, 300.0, 1)),
|
||||
p("light/lightIntensity", "Light Intensity", num(100.0), slider(0.0, 400.0, 0.0, 200.0, 1)),
|
||||
p("light/lightColor", "Light Color", col(1.0, 1.0, 1.0), ParamUi::Color),
|
||||
p("light/lightHeight", "Light Height", num(25.0), slider(-100.0, 100.0, -100.0, 100.0, 1)),
|
||||
p("light/lightDirection", "Light Direction", num(-45.0), ParamUi::Angle),
|
||||
p("shading/ambient", "Ambient", num(0.0), slider(-200.0, 200.0, -100.0, 100.0, 1)),
|
||||
p("shading/diffuse", "Diffuse", num(0.0), slider(0.0, 200.0, 0.0, 100.0, 1)),
|
||||
p("shading/specular", "Specular", num(20.0), slider(0.0, 200.0, 0.0, 100.0, 1)),
|
||||
p("shading/roughness", "Roughness", num(0.05), slider(0.001, 1.0, 0.001, 0.5, 3)),
|
||||
],
|
||||
drizzle,
|
||||
),
|
||||
spec(
|
||||
"ec.sim.ccstarburst",
|
||||
"CC Star Burst",
|
||||
vec![
|
||||
p("scatter", "Scatter", num(100.0), slider(0.0, 1000.0, 0.0, 300.0, 1)),
|
||||
p("speed", "Speed", num(1.0), slider(-10.0, 10.0, -4.0, 4.0, 2)),
|
||||
p("phase", "Phase", num(0.0), ParamUi::Angle),
|
||||
p("gridSpacing", "Grid Spacing", num(2.0), slider(1.0, 100.0, 1.0, 20.0, 0)),
|
||||
p("size", "Size", num(100.0), slider(0.0, 1000.0, 0.0, 300.0, 1)),
|
||||
p("blendWithOriginal", "Blend w. Original", num(0.0), pct()),
|
||||
],
|
||||
star_burst,
|
||||
),
|
||||
spec(
|
||||
"ec.sim.ccparticleworld",
|
||||
"CC Particle World",
|
||||
vec![
|
||||
p("birthRate", "Birth Rate", num(0.5), slider(0.0, 100.0, 0.0, 10.0, 2)),
|
||||
p("longevity", "Longevity (sec)", num(1.0), slider(0.0, 100.0, 0.0, 10.0, 2)),
|
||||
p("producer/producerX", "Position X", num(0.0), slider(-10.0, 10.0, -1.0, 1.0, 2)),
|
||||
p("producer/producerY", "Position Y", num(0.0), slider(-10.0, 10.0, -1.0, 1.0, 2)),
|
||||
p("producer/producerZ", "Position Z", num(0.0), slider(-10.0, 10.0, -1.0, 1.0, 2)),
|
||||
p("producer/radiusX", "Radius X", num(0.025), slider(0.0, 10.0, 0.0, 1.0, 3)),
|
||||
p("producer/radiusY", "Radius Y", num(0.025), slider(0.0, 10.0, 0.0, 1.0, 3)),
|
||||
p("producer/radiusZ", "Radius Z", num(0.025), slider(0.0, 10.0, 0.0, 1.0, 3)),
|
||||
p("physics/animation", "Animation", Value::Enum(0), popup(PW_ANIMS)),
|
||||
p("physics/velocity", "Velocity", num(1.0), slider(-10.0, 10.0, 0.0, 5.0, 2)),
|
||||
p("physics/inheritVelocity", "Inherit Velocity %", num(0.0), slider(-100.0, 100.0, -100.0, 100.0, 1)),
|
||||
p("physics/gravity", "Gravity", num(0.5), slider(-10.0, 10.0, -2.0, 2.0, 3)),
|
||||
p("physics/resistance", "Resistance", num(0.0), slider(0.0, 20.0, 0.0, 5.0, 2)),
|
||||
p("physics/extra", "Extra", num(1.0), slider(0.0, 10.0, 0.0, 2.0, 2)),
|
||||
p("physics/extraAngle", "Extra Angle", num(360.0), ParamUi::Angle),
|
||||
p("physics/directionAxis/axisX", "Axis X", num(0.0), slider(-1.0, 1.0, -1.0, 1.0, 2)),
|
||||
p("physics/directionAxis/axisY", "Axis Y", num(-1.0), slider(-1.0, 1.0, -1.0, 1.0, 2)),
|
||||
p("physics/directionAxis/axisZ", "Axis Z", num(0.0), slider(-1.0, 1.0, -1.0, 1.0, 2)),
|
||||
p("physics/gravityVector/gravityX", "Gravity X", num(0.0), slider(-1.0, 1.0, -1.0, 1.0, 2)),
|
||||
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("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()),
|
||||
p("particle/maxOpacity", "Max Opacity", num(75.0), pct()),
|
||||
p("particle/birthColor", "Birth Color", col(1.0, 1.0, 0.0), ParamUi::Color),
|
||||
p("particle/deathColor", "Death Color", col(0.6, 0.0, 0.0), ParamUi::Color),
|
||||
p("particle/transferMode", "Transfer Mode", Value::Enum(0), popup(&["Composite", "Screen", "Add", "Black Matte"])),
|
||||
p("extras/effectCamera/distance", "Distance", num(2.0), slider(0.0, 100.0, 0.0, 10.0, 2)),
|
||||
p("extras/effectCamera/rotationX", "Rotation X", num(0.0), ParamUi::Angle),
|
||||
p("extras/effectCamera/rotationY", "Rotation Y", num(0.0), ParamUi::Angle),
|
||||
p("extras/effectCamera/rotationZ", "Rotation Z", num(0.0), ParamUi::Angle),
|
||||
p("extras/effectCamera/fov", "FOV", num(PW_FOV), slider(1.0, 179.0, 10.0, 120.0, 1)),
|
||||
p("extras/randomSeed", "Random Seed", num(0.0), slider(0.0, 10_000.0, 0.0, 1000.0, 0)),
|
||||
],
|
||||
particle_world,
|
||||
),
|
||||
spec(
|
||||
"ec.sim.ccparticlesystems2",
|
||||
"CC Particle Systems II",
|
||||
vec![
|
||||
p("birthRate", "Birth Rate", num(2.0), slider(0.0, 100.0, 0.0, 20.0, 2)),
|
||||
p("longevity", "Longevity (sec)", num(1.0), slider(0.0, 100.0, 0.0, 10.0, 2)),
|
||||
p("producer/position", "Position", pt(0.5, 0.5), ParamUi::Point),
|
||||
p("producer/radiusX", "Radius X", num(0.0), slider(0.0, 2000.0, 0.0, 200.0, 1)),
|
||||
p("producer/radiusY", "Radius Y", num(0.0), slider(0.0, 2000.0, 0.0, 200.0, 1)),
|
||||
p("physics/animation", "Animation", Value::Enum(0), popup(PW_ANIMS)),
|
||||
p("physics/velocity", "Velocity", num(2.0), slider(-20.0, 20.0, 0.0, 10.0, 2)),
|
||||
p("physics/inheritVelocity", "Inherit Velocity %", num(0.0), slider(-100.0, 100.0, -100.0, 100.0, 1)),
|
||||
p("physics/gravity", "Gravity", num(1.0), slider(-20.0, 20.0, -5.0, 5.0, 2)),
|
||||
p("physics/resistance", "Resistance", num(0.0), slider(0.0, 20.0, 0.0, 5.0, 2)),
|
||||
p("physics/direction", "Direction", num(0.0), ParamUi::Angle),
|
||||
p("physics/extra", "Extra", num(1.0), slider(0.0, 10.0, 0.0, 2.0, 2)),
|
||||
p("particle/particleType", "Particle Type", Value::Enum(0), popup(PARTICLE_TYPES)),
|
||||
p("particle/birthSize", "Birth Size", num(0.5), slider(0.0, 20.0, 0.0, 5.0, 2)),
|
||||
p("particle/deathSize", "Death Size", num(1.5), slider(0.0, 20.0, 0.0, 5.0, 2)),
|
||||
p("particle/sizeVariation", "Size Variation", num(0.0), pct()),
|
||||
p("particle/maxOpacity", "Max Opacity", num(75.0), pct()),
|
||||
p("particle/birthColor", "Birth Color", col(1.0, 1.0, 0.0), ParamUi::Color),
|
||||
p("particle/deathColor", "Death Color", col(0.6, 0.0, 0.0), ParamUi::Color),
|
||||
p("particle/transferMode", "Transfer Mode", Value::Enum(0), popup(&["Composite", "Screen", "Add", "Black Matte"])),
|
||||
p("randomSeed", "Random Seed", num(0.0), slider(0.0, 10_000.0, 0.0, 1000.0, 0)),
|
||||
],
|
||||
particle_systems2,
|
||||
),
|
||||
spec(
|
||||
"ec.sim.ccmrmercury",
|
||||
"CC Mr. Mercury",
|
||||
vec![
|
||||
p("radiusX", "Radius X", num(50.0), slider(0.0, 2000.0, 0.0, 300.0, 1)),
|
||||
p("radiusY", "Radius Y", num(50.0), slider(0.0, 2000.0, 0.0, 300.0, 1)),
|
||||
p("producer", "Producer", pt(0.5, 0.5), ParamUi::Point),
|
||||
p("direction", "Direction", num(0.0), ParamUi::Angle),
|
||||
p("velocity", "Velocity", num(1.0), slider(-20.0, 20.0, 0.0, 5.0, 2)),
|
||||
p("birthRate", "Birth Rate", num(1.0), slider(0.0, 20.0, 0.0, 5.0, 2)),
|
||||
p("longevity", "Longevity (sec)", num(2.0), slider(0.0, 100.0, 0.0, 10.0, 2)),
|
||||
p("gravity", "Gravity", num(0.5), slider(-20.0, 20.0, -5.0, 5.0, 2)),
|
||||
p("resistance", "Resistance", num(0.0), slider(0.0, 20.0, 0.0, 5.0, 2)),
|
||||
p("extra", "Extra", num(1.0), slider(0.0, 10.0, 0.0, 2.0, 2)),
|
||||
p("animation", "Animation", Value::Enum(0), popup(PW_ANIMS)),
|
||||
p("blobInfluence", "Blob Influence", num(100.0), slider(0.0, 400.0, 0.0, 200.0, 1)),
|
||||
p("blobBirthSize", "Blob Birth Size", num(0.5), slider(0.0, 10.0, 0.0, 2.0, 2)),
|
||||
p("blobDeathSize", "Blob Death Size", num(0.25), slider(0.0, 10.0, 0.0, 2.0, 2)),
|
||||
p("light/lightIntensity", "Light Intensity", num(100.0), slider(0.0, 400.0, 0.0, 200.0, 1)),
|
||||
p("light/lightColor", "Light Color", col(1.0, 1.0, 1.0), ParamUi::Color),
|
||||
p("light/lightHeight", "Light Height", num(50.0), slider(-100.0, 100.0, -100.0, 100.0, 1)),
|
||||
p("light/lightDirection", "Light Direction", num(-45.0), ParamUi::Angle),
|
||||
p("shading/ambient", "Ambient", num(0.0), slider(-200.0, 200.0, -100.0, 100.0, 1)),
|
||||
p("shading/diffuse", "Diffuse", num(75.0), slider(0.0, 200.0, 0.0, 100.0, 1)),
|
||||
p("shading/specular", "Specular", num(50.0), slider(0.0, 200.0, 0.0, 100.0, 1)),
|
||||
p("shading/roughness", "Roughness", num(0.05), slider(0.001, 1.0, 0.001, 0.5, 3)),
|
||||
p("shading/metal", "Metal", num(100.0), pct()),
|
||||
],
|
||||
mr_mercury,
|
||||
),
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,361 @@
|
||||
use super::*;
|
||||
|
||||
// ------------------------------------------------------------------ band rasteriser
|
||||
|
||||
const BAND: usize = 16;
|
||||
|
||||
/// How rasterised items combine with what is already in the target.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum Acc {
|
||||
/// Premultiplied "over" (later items on top).
|
||||
Over,
|
||||
/// Additive (glows, light).
|
||||
Add,
|
||||
}
|
||||
|
||||
/// Rasterise many small items into `img` (buffer pixels). `bbox` gives an item's pixel bounds
|
||||
/// `[x0, y0, x1, y1]` (or `None` to skip), `shade` its premultiplied colour at a pixel centre.
|
||||
/// Items are bucketed into horizontal bands rendered in parallel; within a band they are drawn
|
||||
/// in slice order.
|
||||
pub(crate) fn raster<T: Sync>(
|
||||
img: &mut Image,
|
||||
items: &[T],
|
||||
bbox: impl Fn(&T) -> Option<[f32; 4]> + Sync,
|
||||
shade: impl Fn(&T, f32, f32) -> Option<Px> + Sync,
|
||||
acc: Acc,
|
||||
) {
|
||||
let (w, hgt) = (img.width as usize, img.height as usize);
|
||||
if w == 0 || hgt == 0 || items.is_empty() {
|
||||
return;
|
||||
}
|
||||
let nb = hgt.div_ceil(BAND);
|
||||
let boxes: Vec<Option<[i32; 4]>> = items
|
||||
.par_iter()
|
||||
.map(|it| {
|
||||
let b = bbox(it)?;
|
||||
if !(b[0].is_finite() && b[1].is_finite() && b[2].is_finite() && b[3].is_finite()) {
|
||||
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(w as i32 - 1);
|
||||
let y1 = (b[3].ceil() as i32).min(hgt as i32 - 1);
|
||||
if x1 < x0 || y1 < y0 || b[2] < 0.0 || b[3] < 0.0 {
|
||||
return None;
|
||||
}
|
||||
Some([x0, y0, x1, y1])
|
||||
})
|
||||
.collect();
|
||||
let mut buckets: Vec<Vec<u32>> = vec![Vec::new(); nb];
|
||||
for (i, b) in boxes.iter().enumerate() {
|
||||
if let Some(b) = b {
|
||||
for band in (b[1] as usize / BAND)..=(b[3] as usize / BAND).min(nb - 1) {
|
||||
buckets[band].push(i as u32);
|
||||
}
|
||||
}
|
||||
}
|
||||
img.data.par_chunks_mut(w * BAND).enumerate().for_each(|(band, chunk)| {
|
||||
let by0 = band * BAND;
|
||||
let rows = chunk.len() / w;
|
||||
for &i in &buckets[band] {
|
||||
let Some(b) = boxes[i as usize] else { continue };
|
||||
let it = &items[i as usize];
|
||||
let ya = (b[1] as usize).max(by0);
|
||||
let yb = (b[3] as usize).min(by0 + rows - 1);
|
||||
for y in ya..=yb {
|
||||
let row = &mut chunk[(y - by0) * w..(y - by0 + 1) * w];
|
||||
for x in b[0] as usize..=b[2] as usize {
|
||||
if let Some(c) = shade(it, x as f32 + 0.5, y as f32 + 0.5) {
|
||||
let d = &mut row[x];
|
||||
match acc {
|
||||
Acc::Over => {
|
||||
let k = 1.0 - c[3];
|
||||
*d = [c[0] + d[0] * k, c[1] + d[1] * k, c[2] + d[2] * k, c[3] + d[3] * k];
|
||||
}
|
||||
Acc::Add => {
|
||||
*d = [d[0] + c[0], d[1] + c[1], d[2] + c[2], (d[3] + c[3]).min(1.0)];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ sprites
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Debug)]
|
||||
pub enum Shape {
|
||||
/// Soft round dot (Gaussian-like falloff).
|
||||
Soft,
|
||||
/// Hard anti-aliased disc.
|
||||
Disc,
|
||||
/// Lit sphere (upper-left light, specular).
|
||||
Sphere,
|
||||
/// Disc fading linearly to its rim.
|
||||
Faded,
|
||||
/// Thin ring with a highlight.
|
||||
Bubble,
|
||||
/// Four-point star.
|
||||
Star,
|
||||
/// Rotated square (angle in `rot`).
|
||||
Square,
|
||||
/// Triangle (angle in `rot`).
|
||||
Tri,
|
||||
/// Segment from (x, y) by (dx, dy), half-width `r`.
|
||||
Line { dx: f32, dy: f32 },
|
||||
}
|
||||
|
||||
/// A particle sprite in buffer pixels; `c` is straight colour + opacity.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Sprite {
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
pub r: f32,
|
||||
pub c: [f32; 4],
|
||||
pub shape: Shape,
|
||||
pub rot: f32,
|
||||
}
|
||||
|
||||
impl Sprite {
|
||||
pub(crate) fn new(x: f32, y: f32, r: f32, c: [f32; 4], shape: Shape) -> Sprite {
|
||||
Sprite { x, y, r, c, shape, rot: 0.0 }
|
||||
}
|
||||
}
|
||||
|
||||
/// Where a sprite's colour comes from when it depends on the layer's pixels (resolved by
|
||||
/// [`SpritePlan::resolve`] on the CPU, per pixel by the GPU compositor). Positions are buffer
|
||||
/// pixels.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum Tint {
|
||||
/// The sprite's own colour.
|
||||
Fixed,
|
||||
/// CC Rainfall's refracting drops: where the layer shows (alpha > 0), colour × (layer
|
||||
/// colour × 0.6 + 0.5) sampled at (x, y).
|
||||
Refract { x: f64, y: f64 },
|
||||
/// CC Star Burst: the layer's colour at (x, y), opacity × its alpha (none where clear).
|
||||
Source { x: f64, y: f64 },
|
||||
/// CC Hair: the root's colour at (x, y) mixed with the hair colour (`inherit` = 1 keeps the
|
||||
/// root's), × `shade`, + `spec`; no strand where the root's alpha is below ½.
|
||||
Root { x: f64, y: f64, hair: [f32; 3], inherit: f32, shade: f32, spec: f32 },
|
||||
}
|
||||
|
||||
/// How a sprite layer meets the original layer.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Post {
|
||||
/// `combine` with this mode (0 Composite … 5 sprites only).
|
||||
Combine(u32),
|
||||
/// The sprites alone.
|
||||
Replace,
|
||||
/// The sprites, mixed toward the original by this fraction.
|
||||
Lerp(f32),
|
||||
/// The sprites over opaque black.
|
||||
OverBlack,
|
||||
}
|
||||
|
||||
/// What a sprite-drawing effect draws this frame: built on the CPU (closed-form or from the
|
||||
/// simulation state), rasterised by [`SpritePlan::finish`] or the GPU compositor.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SpritePlan {
|
||||
pub sprites: Vec<Sprite>,
|
||||
/// Per sprite (empty = all [`Tint::Fixed`]).
|
||||
pub tints: Vec<Tint>,
|
||||
pub acc: Acc,
|
||||
pub post: Post,
|
||||
}
|
||||
|
||||
impl SpritePlan {
|
||||
/// Resolve layer-dependent colours against the layer `src`.
|
||||
pub fn resolve(&mut self, src: &Image) {
|
||||
let tints = std::mem::take(&mut self.tints);
|
||||
self.sprites.par_iter_mut().zip(tints.par_iter()).for_each(|(sp, t)| match *t {
|
||||
Tint::Fixed => {}
|
||||
Tint::Refract { x, y } => {
|
||||
let (sc, sa) = unpremul(src.sample_bilinear(x, y));
|
||||
if sa > 0.0 {
|
||||
let c = sp.c;
|
||||
sp.c = [(sc[0] * 0.6 + 0.5) * c[0], (sc[1] * 0.6 + 0.5) * c[1], (sc[2] * 0.6 + 0.5) * c[2], c[3]];
|
||||
}
|
||||
}
|
||||
Tint::Source { x, y } => {
|
||||
let (c, a) = unpremul(src.sample_bilinear(x, y));
|
||||
sp.c = if a <= 0.0 { [0.0; 4] } else { [c[0], c[1], c[2], a * sp.c[3]] };
|
||||
}
|
||||
Tint::Root { x, y, hair, inherit, shade, spec } => {
|
||||
let root = src.sample_bilinear(x, y);
|
||||
if root[3] < 0.5 {
|
||||
sp.c = [0.0; 4];
|
||||
return;
|
||||
}
|
||||
let (rc, _) = unpremul(root);
|
||||
let base: [f32; 3] = std::array::from_fn(|k| rc[k] + (hair[k] - rc[k]) * (1.0 - inherit));
|
||||
sp.c = [base[0] * shade + spec, base[1] * shade + spec, base[2] * shade + spec, sp.c[3]];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Rasterise (after [`SpritePlan::resolve`]) and meet the original layer `b`.
|
||||
pub fn finish(&self, b: Buf) -> Buf {
|
||||
let fx = splat(b.img.width, b.img.height, &self.sprites, self.acc);
|
||||
self.finish_with(b, fx)
|
||||
}
|
||||
|
||||
/// [`finish`](Self::finish) with a prepared sprite image (the motion-blurred average of
|
||||
/// several frames, #1195).
|
||||
pub fn finish_with(&self, b: Buf, fx: Image) -> Buf {
|
||||
self.post.apply(b, fx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Post {
|
||||
/// Meet the sprite image `fx` with the original layer `b` (see [`SpritePlan::finish`]).
|
||||
pub(super) fn apply(self, mut b: Buf, fx: Image) -> Buf {
|
||||
b.img = match self {
|
||||
Post::Combine(m) => combine(&b.img, &fx, m),
|
||||
Post::Replace => fx,
|
||||
Post::Lerp(t) => {
|
||||
let mut o = fx;
|
||||
o.data.par_iter_mut().zip(b.img.data.par_iter()).for_each(|(o, s0)| *o = crate::util::lerp4(*o, *s0, t));
|
||||
o
|
||||
}
|
||||
Post::OverBlack => combine(&Image::filled(b.img.width, b.img.height, [0.0, 0.0, 0.0, 1.0]), &fx, 0),
|
||||
};
|
||||
b
|
||||
}
|
||||
}
|
||||
|
||||
fn sprite_bbox(s: &Sprite) -> Option<[f32; 4]> {
|
||||
if s.c[3] <= 1e-5 || s.r <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
let r = s.r.max(0.5) + 1.0;
|
||||
Some(match s.shape {
|
||||
Shape::Line { dx, dy } => [s.x.min(s.x + dx) - r, s.y.min(s.y + dy) - r, s.x.max(s.x + dx) + r, s.y.max(s.y + dy) + r],
|
||||
Shape::Star => [s.x - r * 2.0, s.y - r * 2.0, s.x + r * 2.0, s.y + r * 2.0],
|
||||
_ => [s.x - r, s.y - r, s.x + r, s.y + r],
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(super) fn aa(edge: f32) -> f32 {
|
||||
(edge + 0.5).clamp(0.0, 1.0)
|
||||
}
|
||||
|
||||
fn sprite_shade(s: &Sprite, x: f32, y: f32) -> Option<Px> {
|
||||
// Sub-pixel sprites keep their energy: draw at radius 0.5 with reduced opacity.
|
||||
let (r, fade) = if s.r < 0.5 { (0.5, s.r / 0.5) } else { (s.r, 1.0) };
|
||||
let (dx, dy) = (x - s.x, y - s.y);
|
||||
let mut rgb = [s.c[0], s.c[1], s.c[2]];
|
||||
let cov = match s.shape {
|
||||
Shape::Soft => {
|
||||
let d2 = (dx * dx + dy * dy) / (r * r);
|
||||
if d2 >= 1.0 {
|
||||
return None;
|
||||
}
|
||||
let q = 1.0 - d2;
|
||||
q * q
|
||||
}
|
||||
Shape::Disc => aa(r - (dx * dx + dy * dy).sqrt()),
|
||||
Shape::Faded => {
|
||||
let d = (dx * dx + dy * dy).sqrt() / r;
|
||||
(1.0 - d).max(0.0).powf(1.5)
|
||||
}
|
||||
Shape::Sphere => {
|
||||
let d = (dx * dx + dy * dy).sqrt();
|
||||
let cv = aa(r - d);
|
||||
if cv <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
let (nx, ny) = (dx / r, dy / r);
|
||||
let nz = (1.0 - nx * nx - ny * ny).max(0.0).sqrt();
|
||||
let (lx, ly, lz) = (-0.48, -0.58, 0.66);
|
||||
let diff = (nx * lx + ny * ly + nz * lz).max(0.0);
|
||||
let rz = 2.0 * diff * nz - lz;
|
||||
let spec = rz.max(0.0).powi(20) * 0.6;
|
||||
let k = 0.2 + 0.8 * diff;
|
||||
rgb = [rgb[0] * k + spec, rgb[1] * k + spec, rgb[2] * k + spec];
|
||||
cv
|
||||
}
|
||||
Shape::Bubble => {
|
||||
let d = (dx * dx + dy * dy).sqrt() / r;
|
||||
if d > 1.0 + 1.0 / r {
|
||||
return None;
|
||||
}
|
||||
let ring = (1.0 - ((d - 0.9) / 0.12).powi(2)).max(0.0);
|
||||
let hl = (1.0 - (((dx / r) + 0.4).powi(2) + ((dy / r) + 0.4).powi(2)) / 0.04).max(0.0);
|
||||
(0.12 * (d <= 1.0) as u8 as f32 + ring * 0.8 + hl).min(1.0)
|
||||
}
|
||||
Shape::Star => {
|
||||
let (sn, cs) = s.rot.sin_cos();
|
||||
let (ux, uy) = ((dx * cs + dy * sn) / r, (-dx * sn + dy * cs) / r);
|
||||
let d = (ux * ux + uy * uy).sqrt();
|
||||
let core = (1.0 - d).max(0.0).powi(2);
|
||||
let sx = (1.0 - ux.abs() / 2.0).max(0.0) * (1.0 - (uy.abs() * 6.0)).max(0.0);
|
||||
let sy = (1.0 - uy.abs() / 2.0).max(0.0) * (1.0 - (ux.abs() * 6.0)).max(0.0);
|
||||
(core + sx + sy).min(1.0)
|
||||
}
|
||||
Shape::Square => {
|
||||
let (sn, cs) = s.rot.sin_cos();
|
||||
let (ux, uy) = (dx * cs + dy * sn, -dx * sn + dy * cs);
|
||||
let e = r * 0.75;
|
||||
aa(e - ux.abs()) * aa(e - uy.abs())
|
||||
}
|
||||
Shape::Tri => {
|
||||
let (sn, cs) = s.rot.sin_cos();
|
||||
let (ux, uy) = (dx * cs + dy * sn, -dx * sn + dy * cs);
|
||||
// Equilateral triangle with circumradius r: three half-planes.
|
||||
let e = r * 0.5;
|
||||
let mut m = f32::INFINITY;
|
||||
for k in 0..3 {
|
||||
let a = std::f32::consts::FRAC_PI_2 + k as f32 * std::f32::consts::TAU / 3.0;
|
||||
let (ns, nc) = a.sin_cos();
|
||||
m = m.min(e - (ux * nc + uy * ns));
|
||||
}
|
||||
aa(m)
|
||||
}
|
||||
Shape::Line { dx: lx, dy: ly } => {
|
||||
let l2 = lx * lx + ly * ly;
|
||||
let t = if l2 > 0.0 { ((dx * lx + dy * ly) / l2).clamp(0.0, 1.0) } else { 0.0 };
|
||||
let (px, py) = (dx - lx * t, dy - ly * t);
|
||||
aa(r - (px * px + py * py).sqrt())
|
||||
}
|
||||
};
|
||||
let a = cov * s.c[3] * fade;
|
||||
if a <= 1e-6 {
|
||||
return None;
|
||||
}
|
||||
Some([rgb[0] * a, rgb[1] * a, rgb[2] * a, a])
|
||||
}
|
||||
|
||||
/// Draw sprites into a new transparent image of `w`×`h`.
|
||||
pub(crate) fn splat(w: u32, hgt: u32, sprites: &[Sprite], acc: Acc) -> Image {
|
||||
let mut img = Image::new(w, hgt);
|
||||
raster(&mut img, sprites, sprite_bbox, sprite_shade, acc);
|
||||
img
|
||||
}
|
||||
|
||||
/// Combine an effect layer `fx` with the original `src` (both premultiplied, same size):
|
||||
/// 0 Composite (fx over src), 1 Screen, 2 Add, 3 Lighten, 4 Darken, 5 fx only.
|
||||
pub(crate) fn combine(src: &Image, fx: &Image, mode: u32) -> Image {
|
||||
let mut out = fx.clone();
|
||||
out.data.par_iter_mut().zip(src.data.par_iter()).for_each(|(o, s)| {
|
||||
let f = *o;
|
||||
*o = match mode {
|
||||
0 => {
|
||||
let k = 1.0 - f[3];
|
||||
[f[0] + s[0] * k, f[1] + s[1] * k, f[2] + s[2] * k, f[3] + s[3] * k]
|
||||
}
|
||||
1 => [s[0] + f[0] - s[0] * f[0], s[1] + f[1] - s[1] * f[1], s[2] + f[2] - s[2] * f[2], s[3] + f[3] - s[3] * f[3]],
|
||||
2 => [s[0] + f[0], s[1] + f[1], s[2] + f[2], (s[3] + f[3]).min(1.0)],
|
||||
3 => [s[0].max(f[0]), s[1].max(f[1]), s[2].max(f[2]), s[3] + f[3] - s[3] * f[3]],
|
||||
4 => {
|
||||
let (fc, fa) = unpremul(f);
|
||||
let m = |c: f32, k: usize| c - (c - c.min(fc[k] * s[3])) * fa;
|
||||
[m(s[0], 0), m(s[1], 1), m(s[2], 2), s[3]]
|
||||
}
|
||||
_ => f,
|
||||
};
|
||||
});
|
||||
out
|
||||
}
|
||||
@@ -0,0 +1,352 @@
|
||||
use super::common::{h, hs};
|
||||
use super::sprites::{Acc, Post, Shape, Sprite, SpritePlan, Tint, aa, raster};
|
||||
use super::*;
|
||||
|
||||
// ------------------------------------------------------------------ CC Rainfall / CC Snowfall
|
||||
|
||||
/// Shared weather layout: per-item depth factor `k` in (0, 1] (1 = nearest) from Scene Depth.
|
||||
#[inline]
|
||||
fn depth_k(z: f32, scene_depth: f32) -> f32 {
|
||||
1.0 / (1.0 + z * scene_depth / 2500.0)
|
||||
}
|
||||
|
||||
/// Wrap `v` into `[lo, lo + span)`.
|
||||
#[inline]
|
||||
fn wrap(v: f32, lo: f32, span: f32) -> f32 {
|
||||
if span <= 0.0 { v } else { lo + (v - lo).rem_euclid(span) }
|
||||
}
|
||||
|
||||
pub(super) fn rainfall(ctx: &EffectCtx, b: Buf) -> Buf {
|
||||
let mut plan = rainfall_plan(ctx, &b);
|
||||
plan.resolve(&b.img);
|
||||
plan.finish(b)
|
||||
}
|
||||
|
||||
pub(super) fn rainfall_plan(ctx: &EffectCtx, b: &Buf) -> SpritePlan {
|
||||
let pr = ctx.params;
|
||||
let n = pr.f("drops").clamp(0.0, 100_000.0) as u32;
|
||||
let size = pr.f("size").max(0.0) as f32;
|
||||
let depth = pr.f("sceneDepth").max(0.0) as f32;
|
||||
let speed = pr.f("speed") as f32;
|
||||
let wind = pr.f("wind") as f32;
|
||||
let wvar = pr.f("windVariation") as f32 / 100.0;
|
||||
let spread = pr.f("spread") as f32;
|
||||
let color = pr.color("color");
|
||||
let opacity = pr.f("opacity") as f32 / 100.0;
|
||||
let mode = pr.e("transferMode");
|
||||
let with_orig = pr.b("compositeWithOriginal");
|
||||
let refract = pr.e("extras/appearance") == 0;
|
||||
let off = pr.v2("extras/offset");
|
||||
let seed = pr.f("extras/randomSeed") as u32 ^ ctx.seed.wrapping_mul(0x9e37);
|
||||
let (lw, lh) = (ctx.layer_size[0] as f32, ctx.layer_size[1] as f32);
|
||||
let s = b.scale as f32;
|
||||
let t = ctx.time as f32;
|
||||
let (ox, oy) = (off[0] as f32 - lw * 0.5, off[1] as f32 - lh * 0.5);
|
||||
let (sprites, tints): (Vec<Sprite>, Vec<Tint>) = (0..n)
|
||||
.into_par_iter()
|
||||
.map(|i| {
|
||||
let z = h(i, 1, seed);
|
||||
let k = depth_k(z, depth);
|
||||
let vy = speed / 4000.0 * lh * 2.2 * k;
|
||||
let w = wind / 1000.0 * lh * (1.0 + wvar * hs(i, 2, seed)) * k + spread * hs(i, 7, seed) * k;
|
||||
let len = (vy * 0.04 * size.max(0.2)).max(1.0);
|
||||
let lenx = w * 0.04 * size.max(0.2);
|
||||
let margin = len + lenx.abs() + 4.0;
|
||||
let y = wrap(h(i, 3, seed) * (lh + 2.0 * margin) + vy * t + oy, -margin, lh + 2.0 * margin);
|
||||
let x = wrap(h(i, 4, seed) * (lw + 2.0 * margin) + w * t + ox, -margin, lw + 2.0 * margin);
|
||||
let (bx, by) = b.to_px([x as f64, y as f64]);
|
||||
let c = [color[0], color[1], color[2], opacity * (0.35 + 0.65 * k)];
|
||||
// Refracting drops show the scene behind them, brightened and offset.
|
||||
let tint = if refract { Tint::Refract { x: bx + 2.0 * s as f64, y: by } } else { Tint::Fixed };
|
||||
let r = (size * 0.6 * k * s).max(0.25);
|
||||
(Sprite::new(bx as f32, by as f32, r, c, Shape::Line { dx: -lenx * s, dy: -len * s }), tint)
|
||||
})
|
||||
.unzip();
|
||||
SpritePlan { sprites, tints, acc: Acc::Over, post: weather_post(mode, with_orig) }
|
||||
}
|
||||
|
||||
fn weather_post(mode: u32, with_orig: bool) -> Post {
|
||||
if with_orig { Post::Combine(if mode == 1 { 3 } else { 0 }) } else { Post::Replace }
|
||||
}
|
||||
|
||||
pub(super) fn snowfall(ctx: &EffectCtx, b: Buf) -> Buf {
|
||||
snowfall_plan(ctx, &b).finish(b)
|
||||
}
|
||||
|
||||
pub(super) fn snowfall_plan(ctx: &EffectCtx, b: &Buf) -> SpritePlan {
|
||||
let pr = ctx.params;
|
||||
let n = pr.f("flakes").clamp(0.0, 200_000.0) as u32;
|
||||
let size = pr.f("size").max(0.0) as f32;
|
||||
let svar = pr.f("sizeVariation") as f32 / 100.0;
|
||||
let depth = pr.f("sceneDepth").max(0.0) as f32;
|
||||
let speed = pr.f("speed") as f32;
|
||||
let spvar = pr.f("speedVariation") as f32 / 100.0;
|
||||
let wind = pr.f("wind") as f32;
|
||||
let wvar = pr.f("windVariation") as f32 / 100.0;
|
||||
let spread = pr.f("spread") as f32;
|
||||
let wig = pr.f("wiggle/wiggleAmount") as f32;
|
||||
let wigvar = pr.f("wiggle/wiggleAmountVariation") as f32 / 100.0;
|
||||
let wfreq = pr.f("wiggle/wiggleFrequency") as f32;
|
||||
let wfvar = pr.f("wiggle/wiggleFrequencyVariation") as f32 / 100.0;
|
||||
let color = pr.color("color");
|
||||
let opacity = pr.f("opacity") as f32 / 100.0;
|
||||
let mode = pr.e("transferMode");
|
||||
let with_orig = pr.b("compositeWithOriginal");
|
||||
let off = pr.v2("extras/offset");
|
||||
let seed = pr.f("extras/randomSeed") as u32 ^ ctx.seed.wrapping_mul(0x85eb);
|
||||
let (lw, lh) = (ctx.layer_size[0] as f32, ctx.layer_size[1] as f32);
|
||||
let s = b.scale as f32;
|
||||
let t = ctx.time as f32;
|
||||
let (ox, oy) = (off[0] as f32 - lw * 0.5, off[1] as f32 - lh * 0.5);
|
||||
let sprites: Vec<Sprite> = (0..n)
|
||||
.into_par_iter()
|
||||
.map(|i| {
|
||||
let z = h(i, 1, seed);
|
||||
let k = depth_k(z, depth);
|
||||
let r = (size * 0.5 * k * (1.0 + svar * hs(i, 2, seed))).max(0.05);
|
||||
let vy = speed * (lh / 600.0) * k * (1.0 + spvar * hs(i, 3, seed));
|
||||
let vx = wind * (lh / 1200.0) * k * (1.0 + wvar * hs(i, 4, seed)) + spread * hs(i, 8, seed) * k;
|
||||
let amp = wig * 2.0 * k * (1.0 + wigvar * hs(i, 5, seed));
|
||||
let fr = wfreq * (1.0 + wfvar * hs(i, 6, seed));
|
||||
let ph = h(i, 9, seed) * std::f32::consts::TAU;
|
||||
let margin = r + amp.abs() + 2.0;
|
||||
let y = wrap(h(i, 10, seed) * (lh + 2.0 * margin) + vy * t + oy, -margin, lh + 2.0 * margin);
|
||||
let x = wrap(h(i, 11, seed) * (lw + 2.0 * margin) + vx * t + ox, -margin, lw + 2.0 * margin) + amp * (fr * t * std::f32::consts::TAU + ph).sin();
|
||||
let (bx, by) = b.to_px([x as f64, y as f64]);
|
||||
Sprite::new(bx as f32, by as f32, r * s, [color[0], color[1], color[2], opacity * (0.4 + 0.6 * k)], Shape::Soft)
|
||||
})
|
||||
.collect();
|
||||
SpritePlan { sprites, tints: vec![], acc: Acc::Over, post: weather_post(mode, with_orig) }
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ CC Bubbles
|
||||
|
||||
/// A CC Bubbles bubble in buffer pixels.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Bubble {
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
pub r: f32,
|
||||
/// Source sample centre (buffer px).
|
||||
pub sx: f32,
|
||||
pub sy: f32,
|
||||
}
|
||||
|
||||
/// CC Bubbles' bubbles this frame (closed-form in time) for buffer geometry `b`.
|
||||
pub fn bubble_list(ctx: &EffectCtx, b: &Buf) -> Vec<Bubble> {
|
||||
let pr = ctx.params;
|
||||
let n = pr.f("bubbleAmount").clamp(0.0, 10_000.0) as u32;
|
||||
let speed = pr.f("bubbleSpeed") as f32;
|
||||
let wamp = pr.f("wobbleAmplitude") as f32;
|
||||
let wfreq = pr.f("wobbleFrequency") as f32;
|
||||
let bsize = pr.f("bubbleSize").max(0.0) as f32;
|
||||
let seed = ctx.seed.wrapping_mul(0x27d4);
|
||||
let (lw, lh) = (ctx.layer_size[0] as f32, ctx.layer_size[1] as f32);
|
||||
let s = b.scale as f32;
|
||||
let t = ctx.time as f32;
|
||||
let base = lw.min(lh) * 0.035 * bsize;
|
||||
(0..n)
|
||||
.map(|i| {
|
||||
let r = base * (0.5 + h(i, 1, seed));
|
||||
let travel = lh + 2.0 * r;
|
||||
let v = speed * lh * 0.3 * (0.6 + 0.8 * h(i, 2, seed));
|
||||
let y = lh + r - (h(i, 3, seed) * travel + v * t).rem_euclid(travel);
|
||||
let x0 = h(i, 4, seed) * lw;
|
||||
let x = x0 + wamp * (r / base.max(1e-3)) * (t * wfreq * std::f32::consts::TAU + h(i, 5, seed) * std::f32::consts::TAU).sin();
|
||||
let (bx, by) = b.to_px([x as f64, y as f64]);
|
||||
let (sx, sy) = b.to_px([x0 as f64, y as f64]);
|
||||
Bubble { x: bx as f32, y: by as f32, r: r * s, sx: sx as f32, sy: sy as f32 }
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(super) fn bubbles(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let pr = ctx.params;
|
||||
let metal = pr.e("reflectionType") == 1;
|
||||
let shading = pr.e("shadingType");
|
||||
let list = bubble_list(ctx, &b);
|
||||
let src = b.img.clone();
|
||||
let mut out = Image::new(src.width, src.height);
|
||||
raster(
|
||||
&mut out,
|
||||
&list,
|
||||
|q| Some([q.x - q.r - 1.0, q.y - q.r - 1.0, q.x + q.r + 1.0, q.y + q.r + 1.0]),
|
||||
|q, x, y| {
|
||||
let (dx, dy) = ((x - q.x) / q.r.max(0.5), (y - q.y) / q.r.max(0.5));
|
||||
let d2 = dx * dx + dy * dy;
|
||||
let d = d2.sqrt();
|
||||
let cov = aa((1.0 - d) * q.r);
|
||||
if cov <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
// Liquid refracts (inverted, magnified), metal reflects (wide-angle mirror).
|
||||
let k = if metal { 1.6 } else { -0.7 };
|
||||
let nz = (1.0 - d2).max(0.0).sqrt();
|
||||
let p = src.sample_bilinear((q.sx + dx * q.r * k) as f64, (q.sy + dy * q.r * k) as f64);
|
||||
let (c, sa) = unpremul(p);
|
||||
let mut c = if sa <= 0.0 { [0.6, 0.7, 0.8] } else { c };
|
||||
let mut a = cov;
|
||||
match shading {
|
||||
1 => c = c.map(|v| v * 0.7 + 0.3 * (1.0 - nz)),
|
||||
2 => c = c.map(|v| v * (0.5 + 0.5 * nz)),
|
||||
3 => a *= nz.powf(0.5),
|
||||
4 => a *= 1.0 - nz * 0.8,
|
||||
_ => {}
|
||||
}
|
||||
let hl = (1.0 - ((dx + 0.35).powi(2) + (dy + 0.35).powi(2)) / 0.05).max(0.0) * 0.8;
|
||||
let c = c.map(|v| v + hl);
|
||||
Some([c[0] * a, c[1] * a, c[2] * a, a])
|
||||
},
|
||||
Acc::Over,
|
||||
);
|
||||
b.img = out;
|
||||
b
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ CC Drizzle
|
||||
|
||||
/// CC Drizzle's live drops this frame (buffer px: centre x, y, ring radius, amplitude);
|
||||
/// `None` = no drops yet (the layer passes through).
|
||||
pub fn drizzle_drops(ctx: &EffectCtx, b: &Buf) -> Option<Vec<[f32; 4]>> {
|
||||
let pr = ctx.params;
|
||||
let rate = pr.f("dripRate").max(0.0);
|
||||
let life = pr.f("longevity").max(0.01);
|
||||
let spreading = pr.f("spreading") as f32;
|
||||
let height = pr.f("rippleHeight") as f32 / 100.0;
|
||||
let seed = ctx.seed.wrapping_mul(0x165667b1);
|
||||
let t = ctx.time;
|
||||
let (lw, lh) = (ctx.layer_size[0] as f32, ctx.layer_size[1] as f32);
|
||||
let s = b.scale as f32;
|
||||
if rate <= 0.0 || t < 0.0 {
|
||||
return None;
|
||||
}
|
||||
// Drops born at k / rate with hashed positions; alive while younger than the longevity.
|
||||
let k0 = ((t - life) * rate).floor().max(0.0) as u32;
|
||||
let k1 = (t * rate).floor() as u32;
|
||||
let mut drops = Vec::new();
|
||||
for k in k0..=k1 {
|
||||
let born = k as f64 / rate;
|
||||
let age = (t - born) as f32;
|
||||
if age < 0.0 || age as f64 >= life {
|
||||
continue;
|
||||
}
|
||||
let (x, y) = b.to_px([(h(k, 1, seed) * lw) as f64, (h(k, 2, seed) * lh) as f64]);
|
||||
let radius = spreading * age * lh / 400.0 * s;
|
||||
let amp = height * (1.0 - age / life as f32) * (0.6 + 0.4 * h(k, 3, seed));
|
||||
drops.push([x as f32, y as f32, radius, amp]);
|
||||
}
|
||||
Some(drops)
|
||||
}
|
||||
|
||||
pub(super) fn drizzle(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let pr = ctx.params;
|
||||
let Some(drops) = drizzle_drops(ctx, &b) else { return b };
|
||||
let rippling = (pr.f("rippling") / 360.0).max(0.05) as f32;
|
||||
let disp = pr.f("displacement") as f32;
|
||||
let li = pr.f("light/lightIntensity") as f32 / 100.0;
|
||||
let lc = pr.color("light/lightColor");
|
||||
let lheight = pr.f("light/lightHeight") as f32 / 100.0;
|
||||
let ldir = (pr.f("light/lightDirection") as f32).to_radians();
|
||||
let ambient = pr.f("shading/ambient") as f32 / 100.0;
|
||||
let diffuse = pr.f("shading/diffuse") as f32 / 100.0;
|
||||
let specular = pr.f("shading/specular") as f32 / 100.0;
|
||||
let rough = pr.f("shading/roughness").max(0.001) as f32;
|
||||
let lh = ctx.layer_size[1] as f32;
|
||||
let s = b.scale as f32;
|
||||
let wavelen = (lh * 0.04 * s).max(2.0) / rippling.clamp(0.25, 4.0);
|
||||
let width = wavelen * (1.0 + rippling * 2.0);
|
||||
let field = |x: f32, y: f32| -> f32 {
|
||||
let mut v = 0.0;
|
||||
for &[dx, dy, r, a] in &drops {
|
||||
let d = ((x - dx).powi(2) + (y - dy).powi(2)).sqrt();
|
||||
let u = (d - r) / width;
|
||||
if u.abs() < 1.0 {
|
||||
let env = 0.5 + 0.5 * (u * std::f32::consts::PI).cos();
|
||||
v += a * env * ((d - r) / wavelen * std::f32::consts::TAU).cos();
|
||||
}
|
||||
}
|
||||
v
|
||||
};
|
||||
let src = b.img.clone();
|
||||
let (ls, lcs) = ldir.sin_cos();
|
||||
let l = {
|
||||
let v = [lcs, ls, lheight.max(0.05)];
|
||||
let n = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
|
||||
[v[0] / n, v[1] / n, v[2] / n]
|
||||
};
|
||||
b.img.rows_mut().for_each(|(y, row)| {
|
||||
for (x, px) in row.iter_mut().enumerate() {
|
||||
let (fx, fy) = (x as f32 + 0.5, y as f32 + 0.5);
|
||||
let gx = field(fx + 1.0, fy) - field(fx - 1.0, fy);
|
||||
let gy = field(fx, fy + 1.0) - field(fx, fy - 1.0);
|
||||
if gx == 0.0 && gy == 0.0 {
|
||||
continue;
|
||||
}
|
||||
let p = src.sample_bilinear_clamped((fx - gx * disp * s * 4.0) as f64, (fy - gy * disp * s * 4.0) as f64);
|
||||
let n = {
|
||||
let v = [-gx * 6.0, -gy * 6.0, 1.0];
|
||||
let m = (v[0] * v[0] + v[1] * v[1] + 1.0).sqrt();
|
||||
[v[0] / m, v[1] / m, v[2] / m]
|
||||
};
|
||||
let nl = (n[0] * l[0] + n[1] * l[1] + n[2] * l[2]).max(0.0);
|
||||
let hz = (l[2] + 1.0) * 0.5;
|
||||
let hv = {
|
||||
let v = [l[0] * 0.5, l[1] * 0.5, hz];
|
||||
let m = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
|
||||
[v[0] / m, v[1] / m, v[2] / m]
|
||||
};
|
||||
let sp = (n[0] * hv[0] + n[1] * hv[1] + n[2] * hv[2]).max(0.0).powf(1.0 / rough) * specular * li;
|
||||
let shade = 1.0 + (ambient + diffuse * nl - diffuse * l[2]) * li;
|
||||
let a = p[3];
|
||||
*px = [p[0] * shade + sp * lc[0] * a, p[1] * shade + sp * lc[1] * a, p[2] * shade + sp * lc[2] * a, a];
|
||||
}
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ CC Star Burst
|
||||
|
||||
pub(super) fn star_burst(ctx: &EffectCtx, b: Buf) -> Buf {
|
||||
let mut plan = star_burst_plan(ctx, &b);
|
||||
plan.resolve(&b.img);
|
||||
plan.finish(b)
|
||||
}
|
||||
|
||||
pub(super) fn star_burst_plan(ctx: &EffectCtx, b: &Buf) -> SpritePlan {
|
||||
let pr = ctx.params;
|
||||
let scatter = pr.f("scatter") as f32;
|
||||
let speed = pr.f("speed") as f32;
|
||||
let phase = pr.f("phase") as f32 / 360.0;
|
||||
let spacing = pr.f("gridSpacing").max(1.0) as f32;
|
||||
let size = pr.f("size") as f32 / 100.0;
|
||||
let blend = pr.f("blendWithOriginal") as f32 / 100.0;
|
||||
let seed = ctx.seed.wrapping_mul(0x3c6e);
|
||||
let (lw, lh) = (ctx.layer_size[0] as f32, ctx.layer_size[1] as f32);
|
||||
let s = b.scale as f32;
|
||||
let t = ctx.time as f32;
|
||||
let nx = (lw / spacing).ceil().max(1.0) as u32;
|
||||
let ny = (lh / spacing).ceil().max(1.0) as u32;
|
||||
let (cx, cy) = (lw * 0.5, lh * 0.5);
|
||||
let mut list: Vec<(f32, Sprite, Tint)> = (0..nx * ny)
|
||||
.into_par_iter()
|
||||
.map(|i| {
|
||||
let (gx, gy) = ((i % nx) as f32 * spacing + spacing * 0.5, (i / nx) as f32 * spacing + spacing * 0.5);
|
||||
let (sx, sy) = b.to_px([gx as f64, gy as f64]);
|
||||
// Depth cycles towards the viewer: z in [0, 1), perspective 1 / (1 - 0.9 z).
|
||||
let z = (h(i, 1, seed) + t * speed * 0.25 + phase).rem_euclid(1.0);
|
||||
let persp = 1.0 / (1.0 - 0.9 * z);
|
||||
let jx = hs(i, 2, seed) * scatter * 0.01 * lw * 0.5;
|
||||
let jy = hs(i, 3, seed) * scatter * 0.01 * lh * 0.5;
|
||||
let x = cx + (gx - cx + jx) * persp;
|
||||
let y = cy + (gy - cy + jy) * persp;
|
||||
let (bx, by) = b.to_px([x as f64, y as f64]);
|
||||
let r = spacing * 0.5 * size * persp * s;
|
||||
let fade = (z * 8.0).min(1.0) * ((1.0 - z) * 6.0).min(1.0);
|
||||
// The layer's colour at the grid point (none where it is clear).
|
||||
(z, Sprite::new(bx as f32, by as f32, r, [0.0, 0.0, 0.0, fade], Shape::Disc), Tint::Source { x: sx, y: sy })
|
||||
})
|
||||
.collect();
|
||||
list.sort_by(|a, b| a.0.total_cmp(&b.0));
|
||||
let (sprites, tints) = list.into_iter().map(|(_, s, t)| (s, t)).unzip();
|
||||
SpritePlan { sprites, tints, acc: Acc::Over, post: if blend > 0.0 { Post::Lerp(blend) } else { Post::Replace } }
|
||||
}
|
||||
Reference in New Issue
Block a user