M9.3: Stylize effects (CC Block Load, CC Burn Film, CC Glass, CC HexTile, CC Mr. Smoothie, CC Plastic)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
685d6bcd04
commit
8503f816ae
|
|
@ -1,7 +1,659 @@
|
|||
//! Stylize effects, batch 3.
|
||||
//! Stylize effects, batch 3: CC Block Load, CC Burn Film, CC Glass, CC HexTile, CC Mr. Smoothie
|
||||
//! and CC Plastic.
|
||||
//!
|
||||
//! Written from the public descriptions of what these effects do (progressive block loading,
|
||||
//! burning film, bump-mapped glass / plastic surfaces lit by an effect light, hexagonal tiling,
|
||||
//! a colour map sampled from the image itself); the maths is our own.
|
||||
|
||||
use crate::EffectSpec;
|
||||
use effectcraft_color::rgb_to_hsl;
|
||||
use effectcraft_keyframe::Value;
|
||||
use effectcraft_project::ParamUi;
|
||||
use effectcraft_raster::{Image, Px};
|
||||
use rayon::prelude::*;
|
||||
|
||||
use crate::noise::fbm;
|
||||
use crate::util::{Plane, gauss_plane, layer_or_self, lerp4, premul, smoothstep, unpremul};
|
||||
use crate::{Buf, EffectCtx, EffectSpec, col, num, p, popup, slider};
|
||||
|
||||
fn spec(id: &'static str, name: &'static str, params: Vec<crate::ParamSpec>, render: crate::RenderFn) -> EffectSpec {
|
||||
EffectSpec { id, name, category: "Stylize", params, render, gpu: false, float: true }
|
||||
}
|
||||
|
||||
fn pct() -> ParamUi {
|
||||
slider(0.0, 100.0, 0.0, 100.0, 1)
|
||||
}
|
||||
|
||||
fn pt(x: f64, y: f64) -> Value {
|
||||
Value::Vec2([x, y])
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ CC Block Load
|
||||
|
||||
/// Mean colour of every `s`×`s` block (grid of `ceil(w/s)`×`ceil(h/s)`).
|
||||
fn block_means(img: &Image, s: usize) -> (usize, usize, Vec<Px>) {
|
||||
let (w, h) = (img.width as usize, img.height as usize);
|
||||
let gw = w.div_ceil(s).max(1);
|
||||
let gh = h.div_ceil(s).max(1);
|
||||
let mut out = vec![[0.0f32; 4]; gw * gh];
|
||||
out.par_chunks_mut(gw).enumerate().for_each(|(by, row)| {
|
||||
for (bx, o) in row.iter_mut().enumerate() {
|
||||
let mut acc = [0.0f32; 4];
|
||||
let mut n = 0.0f32;
|
||||
for y in by * s..((by + 1) * s).min(h) {
|
||||
for x in bx * s..((bx + 1) * s).min(w) {
|
||||
let p = img.data[y * w + x];
|
||||
for c in 0..4 {
|
||||
acc[c] += p[c];
|
||||
}
|
||||
n += 1.0;
|
||||
}
|
||||
}
|
||||
if n > 0.0 {
|
||||
*o = acc.map(|v| v / n);
|
||||
}
|
||||
}
|
||||
});
|
||||
(gw, gh, out)
|
||||
}
|
||||
|
||||
/// Value of pixel (x, y) at block size `s` (nearest block, or bilinear between block centres).
|
||||
fn block_at(g: &(usize, usize, Vec<Px>), s: usize, x: usize, y: usize, bilinear: bool) -> Px {
|
||||
let (gw, gh, d) = g;
|
||||
if !bilinear || s == 1 {
|
||||
return d[(y / s).min(gh - 1) * gw + (x / s).min(gw - 1)];
|
||||
}
|
||||
let fx = (x as f32 + 0.5) / s as f32 - 0.5;
|
||||
let fy = (y as f32 + 0.5) / s as f32 - 0.5;
|
||||
let x0 = fx.floor();
|
||||
let y0 = fy.floor();
|
||||
let (tx, ty) = (fx - x0, fy - y0);
|
||||
let gx = |v: f32| (v as i64).clamp(0, *gw as i64 - 1) as usize;
|
||||
let gy = |v: f32| (v as i64).clamp(0, *gh as i64 - 1) as usize;
|
||||
let a = d[gy(y0) * gw + gx(x0)];
|
||||
let b = d[gy(y0) * gw + gx(x0 + 1.0)];
|
||||
let c = d[gy(y0 + 1.0) * gw + gx(x0)];
|
||||
let e = d[gy(y0 + 1.0) * gw + gx(x0 + 1.0)];
|
||||
lerp4(lerp4(a, b, tx), lerp4(c, e, tx), ty)
|
||||
}
|
||||
|
||||
fn block_load(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let c = (ctx.params.f("completion") / 100.0).clamp(0.0, 1.0);
|
||||
if c >= 1.0 {
|
||||
return b;
|
||||
}
|
||||
let scan = ctx.params.b("scanlines");
|
||||
let smooth = ctx.params.b("smoothing");
|
||||
let cleared = ctx.params.b("startCleared");
|
||||
let bilinear = ctx.params.b("bilinear");
|
||||
let (w, h) = (b.img.width as usize, b.img.height as usize);
|
||||
let nlev = (w.max(h).max(2) as f64).log2().ceil() as u32;
|
||||
// Levels 0..=nlev with block sizes 2^(nlev-k); level k is "loading" while p is in [k, k+1).
|
||||
let p = c * (nlev + 1) as f64;
|
||||
let done = p.floor() as u32;
|
||||
let frac = (p - done as f64) as f32;
|
||||
let size = |k: u32| 1usize << (nlev - k.min(nlev));
|
||||
let s_new = size(done);
|
||||
let new = block_means(&b.img, s_new);
|
||||
let old = if done > 0 { Some((size(done - 1), block_means(&b.img, size(done - 1)))) } else { None };
|
||||
let rows_new = h.div_ceil(s_new) as f32;
|
||||
let soft = if smooth { 1.5f32 } else { 0.0 };
|
||||
let mut out = Image::new(w as u32, h as u32);
|
||||
out.rows_mut().for_each(|(y, row)| {
|
||||
// Fraction of the new level present on this row.
|
||||
let t = if scan {
|
||||
let front = frac * (rows_new + soft) - soft * 0.5;
|
||||
let r = (y / s_new) as f32 + 0.5;
|
||||
if soft > 0.0 {
|
||||
smoothstep(r - soft * 0.5, r + soft * 0.5, front)
|
||||
} else if r < front {
|
||||
1.0
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
} else if smooth {
|
||||
frac
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
for (x, o) in row.iter_mut().enumerate() {
|
||||
let n = if t > 0.0 { block_at(&new, s_new, x, y, bilinear) } else { [0.0; 4] };
|
||||
let prev = match &old {
|
||||
Some((s, g)) => block_at(g, *s, x, y, bilinear),
|
||||
None if cleared => [0.0; 4],
|
||||
None => block_at(&new, s_new, x, y, bilinear),
|
||||
};
|
||||
*o = if t >= 1.0 {
|
||||
n
|
||||
} else if t <= 0.0 {
|
||||
prev
|
||||
} else {
|
||||
lerp4(prev, n, t)
|
||||
};
|
||||
}
|
||||
});
|
||||
b.img = out;
|
||||
b
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ CC Burn Film
|
||||
|
||||
fn burn_film(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let burn = (ctx.params.f("burn") / 100.0).clamp(0.0, 1.0) as f32;
|
||||
if burn <= 0.0 {
|
||||
return b;
|
||||
}
|
||||
let (cx, cy) = b.to_px(ctx.params.v2("center"));
|
||||
let seed = ctx.params.f("randomSeed").max(0.0) as u32;
|
||||
let s = b.scale;
|
||||
let diag = (ctx.layer_size[0].hypot(ctx.layer_size[1]) * s).max(1.0);
|
||||
let feat = (60.0 * s).max(1e-3);
|
||||
let th = 1.0 - burn * 1.25;
|
||||
let band = 0.12 * (burn * 10.0).min(1.0);
|
||||
b.img.rows_mut().for_each(|(y, row)| {
|
||||
for (x, px) in row.iter_mut().enumerate() {
|
||||
let (fx, fy) = (x as f64 + 0.5, y as f64 + 0.5);
|
||||
let d = ((fx - cx).hypot(fy - cy) / diag) as f32;
|
||||
let n = fbm((fx / feat) as f32, (fy / feat) as f32, 0.0, seed.wrapping_add(17), 4.0);
|
||||
// High near the centre and where the noise is high; burning eats from the top down.
|
||||
let f = n * 0.65 + (1.0 - d.min(1.0)) * 0.35;
|
||||
if f > th + band {
|
||||
*px = [0.0; 4];
|
||||
continue;
|
||||
}
|
||||
if f > th - band * 2.0 {
|
||||
let (c, a) = unpremul(*px);
|
||||
// Charred rim (scorched dark) with a hot orange edge right next to the hole.
|
||||
let k = smoothstep(th - band * 2.0, th, f);
|
||||
let hot = smoothstep(th, th + band, f);
|
||||
let char = 1.0 - k * 0.85;
|
||||
let mut o = [c[0] * char, c[1] * char, c[2] * char];
|
||||
o = [o[0] + (1.0 - o[0]) * hot * 0.9, o[1] + (0.45 - o[1]) * hot * 0.9, o[2] * (1.0 - hot)];
|
||||
let na = a * (1.0 - hot * 0.6);
|
||||
*px = premul(o, na);
|
||||
}
|
||||
}
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ bump-map surfaces (Glass, Plastic)
|
||||
|
||||
const BUMP_PROPS: [&str; 6] = ["Red", "Green", "Blue", "Alpha", "Luminance", "Lightness"];
|
||||
|
||||
/// Height field (0..1) from the bump-map layer (or the layer itself), softened.
|
||||
fn height_field(ctx: &EffectCtx, b: &Buf) -> Plane {
|
||||
let src = layer_or_self(ctx, b, "bumpMap", true, false);
|
||||
let prop = ctx.params.e("property");
|
||||
let mut pl = Plane::from_image(&src, |p| {
|
||||
let (c, a) = unpremul(p);
|
||||
let v = match prop {
|
||||
0 => c[0],
|
||||
1 => c[1],
|
||||
2 => c[2],
|
||||
3 => return a,
|
||||
4 => effectcraft_color::luminance(c[0], c[1], c[2]),
|
||||
_ => rgb_to_hsl(c[0], c[1], c[2]).2,
|
||||
};
|
||||
v * a
|
||||
});
|
||||
let soft = ctx.params.f("softness") * b.scale * 0.5;
|
||||
if soft > 0.05 {
|
||||
pl = gauss_plane(&pl, soft, soft);
|
||||
}
|
||||
pl
|
||||
}
|
||||
|
||||
struct Light {
|
||||
intensity: f32,
|
||||
color: [f32; 3],
|
||||
point: bool,
|
||||
pos: (f64, f64),
|
||||
/// Distant light direction (towards the light).
|
||||
dir: [f32; 3],
|
||||
height: f32,
|
||||
ambient: f32,
|
||||
diffuse: f32,
|
||||
specular: f32,
|
||||
shininess: f32,
|
||||
metal: f32,
|
||||
}
|
||||
|
||||
impl Light {
|
||||
fn from(ctx: &EffectCtx, b: &Buf) -> Light {
|
||||
let pr = &ctx.params;
|
||||
let elev = (pr.f("lightHeight").clamp(-100.0, 100.0) / 100.0 * 90.0).to_radians();
|
||||
let az = pr.f("lightDirection").to_radians();
|
||||
let dir = [(az.sin() * elev.cos()) as f32, (-az.cos() * elev.cos()) as f32, elev.sin() as f32];
|
||||
let c = pr.color("lightColor");
|
||||
let rough = pr.f("roughness").clamp(0.001, 1.0) as f32;
|
||||
Light {
|
||||
intensity: (pr.f("lightIntensity") / 100.0) as f32,
|
||||
color: [c[0], c[1], c[2]],
|
||||
point: pr.e("lightType") == 1,
|
||||
pos: b.to_px(pr.v2("lightPosition")),
|
||||
dir,
|
||||
height: (pr.f("lightHeight") / 100.0 * ctx.layer_size[0].max(ctx.layer_size[1]) * b.scale * 0.5) as f32,
|
||||
ambient: (pr.f("ambient") / 100.0) as f32,
|
||||
diffuse: (pr.f("diffuse") / 100.0) as f32,
|
||||
specular: (pr.f("specular") / 100.0) as f32,
|
||||
shininess: 1.0 / rough,
|
||||
metal: (pr.f("metal") / 100.0) as f32,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_light(&self, x: f64, y: f64) -> [f32; 3] {
|
||||
if self.point {
|
||||
let v = [(self.pos.0 - x) as f32, (self.pos.1 - y) as f32, self.height.max(1.0)];
|
||||
let l = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt().max(1e-6);
|
||||
[v[0] / l, v[1] / l, v[2] / l]
|
||||
} else {
|
||||
self.dir
|
||||
}
|
||||
}
|
||||
|
||||
/// Lit straight colour for surface normal `n`. Diffuse is normalised so that a flat surface
|
||||
/// keeps its colour; highlights add on top.
|
||||
fn shade(&self, c: [f32; 3], n: [f32; 3], l: [f32; 3]) -> [f32; 3] {
|
||||
let ndl = (n[0] * l[0] + n[1] * l[1] + n[2] * l[2]).max(0.0);
|
||||
let flat = l[2].max(0.0);
|
||||
let base = self.ambient + self.diffuse * flat;
|
||||
let lit = if base > 1e-4 { (self.ambient + self.diffuse * ndl) / base } else { 1.0 };
|
||||
let lit = 1.0 + (lit - 1.0) * self.intensity;
|
||||
// Blinn half vector with the viewer straight above.
|
||||
let hv = [l[0], l[1], l[2] + 1.0];
|
||||
let hl = (hv[0] * hv[0] + hv[1] * hv[1] + hv[2] * hv[2]).sqrt().max(1e-6);
|
||||
let ndh = ((n[0] * hv[0] + n[1] * hv[1] + n[2] * hv[2]) / hl).max(0.0);
|
||||
let spec = self.specular * self.intensity * ndh.powf(self.shininess);
|
||||
let mut o = [0.0f32; 3];
|
||||
for i in 0..3 {
|
||||
let hl_col = self.color[i] * (1.0 - self.metal) + c[i] * self.color[i] * self.metal;
|
||||
o[i] = c[i] * lit * (1.0 + (self.color[i] - 1.0) * self.intensity.min(1.0)) + spec * hl_col;
|
||||
}
|
||||
o
|
||||
}
|
||||
}
|
||||
|
||||
/// Surface normal from a height field at pixel (x, y) with height scale `k`.
|
||||
#[inline]
|
||||
fn normal(h: &Plane, x: usize, y: usize, k: f32) -> [f32; 3] {
|
||||
let (xi, yi) = (x as i64, y as i64);
|
||||
let gx = (h.get_clamped(xi + 1, yi) - h.get_clamped(xi - 1, yi)) * 0.5 * k;
|
||||
let gy = (h.get_clamped(xi, yi + 1) - h.get_clamped(xi, yi - 1)) * 0.5 * k;
|
||||
let l = (gx * gx + gy * gy + 1.0).sqrt();
|
||||
[-gx / l, -gy / l, 1.0 / l]
|
||||
}
|
||||
|
||||
fn glass(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let hf = height_field(ctx, &b);
|
||||
let height = ctx.params.f("height") as f32 / 100.0;
|
||||
let k = height * 25.0;
|
||||
let disp = ctx.params.f("displacement") * b.scale * 0.25;
|
||||
let light = Light::from(ctx, &b);
|
||||
let src = b.img.clone();
|
||||
b.img.rows_mut().for_each(|(y, row)| {
|
||||
for (x, px) in row.iter_mut().enumerate() {
|
||||
let n = normal(&hf, x, y, k);
|
||||
let (fx, fy) = (x as f64 + 0.5, y as f64 + 0.5);
|
||||
let s = if disp != 0.0 && height != 0.0 {
|
||||
src.sample_bilinear_clamped(fx + n[0] as f64 * disp, fy + n[1] as f64 * disp)
|
||||
} else {
|
||||
src.data[y * src.width as usize + x]
|
||||
};
|
||||
let a = src.data[y * src.width as usize + x][3];
|
||||
if a <= 0.0 {
|
||||
*px = [0.0; 4];
|
||||
continue;
|
||||
}
|
||||
let (c, sa) = unpremul(s);
|
||||
let c = if sa > 0.0 { c } else { unpremul(src.data[y * src.width as usize + x]).0 };
|
||||
let o = light.shade(c, n, light.to_light(fx, fy));
|
||||
*px = premul(o, a);
|
||||
}
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
fn plastic(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let mut hf = height_field(ctx, &b);
|
||||
let lo = (ctx.params.f("cutMin") / 100.0) as f32;
|
||||
let hi = (ctx.params.f("cutMax") / 100.0) as f32;
|
||||
if lo > 0.0 || hi < 1.0 {
|
||||
hf = hf.map(|v| v.clamp(lo.min(hi), hi.max(lo)));
|
||||
}
|
||||
let k = ctx.params.f("height") as f32 / 100.0 * 25.0;
|
||||
let light = Light::from(ctx, &b);
|
||||
b.img.rows_mut().for_each(|(y, row)| {
|
||||
for (x, px) in row.iter_mut().enumerate() {
|
||||
if px[3] <= 0.0 {
|
||||
continue;
|
||||
}
|
||||
let (c, a) = unpremul(*px);
|
||||
let n = normal(&hf, x, y, k);
|
||||
let o = light.shade(c, n, light.to_light(x as f64 + 0.5, y as f64 + 0.5));
|
||||
*px = premul(o, a);
|
||||
}
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ CC HexTile
|
||||
|
||||
/// Axial hex cell (pointy-top) containing (x, y) for circumradius `r`; returns the cell centre
|
||||
/// and integer cell coordinates.
|
||||
fn hex_cell(x: f64, y: f64, r: f64) -> ((f64, f64), (i64, i64)) {
|
||||
let q = (3f64.sqrt() / 3.0 * x - y / 3.0) / r;
|
||||
let rr = (2.0 / 3.0 * y) / r;
|
||||
// Cube rounding.
|
||||
let (cx, cz) = (q, rr);
|
||||
let cy = -cx - cz;
|
||||
let (mut rx, ry, mut rz) = (cx.round(), cy.round(), cz.round());
|
||||
let (dx, dy, dz) = ((rx - cx).abs(), (ry - cy).abs(), (rz - cz).abs());
|
||||
if dx > dy && dx > dz {
|
||||
rx = -ry - rz;
|
||||
} else if dy <= dz {
|
||||
rz = -rx - ry;
|
||||
}
|
||||
let px = r * 3f64.sqrt() * (rx + rz / 2.0);
|
||||
let py = r * 1.5 * rz;
|
||||
((px, py), (rx as i64, rz as i64))
|
||||
}
|
||||
|
||||
fn hextile(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let r = (ctx.params.f("radius") * b.scale).max(0.5);
|
||||
let (cx, cy) = b.to_px(ctx.params.v2("center"));
|
||||
let rot = ctx.params.f("rotate").to_radians();
|
||||
let (sn, cs) = rot.sin_cos();
|
||||
let smear = (ctx.params.f("smearing") / 100.0).clamp(0.0, 1.0);
|
||||
let mode = ctx.params.e("render");
|
||||
let src = b.img.clone();
|
||||
b.img.rows_mut().for_each(|(y, row)| {
|
||||
for (x, px) in row.iter_mut().enumerate() {
|
||||
let (dx, dy) = (x as f64 + 0.5 - cx, y as f64 + 0.5 - cy);
|
||||
// Into the rotated tiling frame.
|
||||
let (u, v) = (dx * cs + dy * sn, -dx * sn + dy * cs);
|
||||
let ((hx, hy), (qi, ri)) = hex_cell(u, v, r);
|
||||
let (mut lu, lv) = (u - hx, v - hy);
|
||||
let odd = (qi + ri).rem_euclid(2) == 1;
|
||||
match mode {
|
||||
1 if odd => lu = -lu,
|
||||
2 if odd => lu = -lu,
|
||||
_ => {}
|
||||
}
|
||||
let lv = if mode == 2 && odd { -lv } else { lv };
|
||||
// Every tile shows the hexagon around the centre point.
|
||||
let (ox, oy) = (lu * cs - lv * sn, lu * sn + lv * cs);
|
||||
let (tx, ty) = (cx + ox, cy + oy);
|
||||
let (sx, sy) = (tx + (x as f64 + 0.5 - tx) * smear, ty + (y as f64 + 0.5 - ty) * smear);
|
||||
*px = src.sample_bilinear_clamped(sx, sy);
|
||||
}
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ CC Mr. Smoothie
|
||||
|
||||
const SMOOTHIE_PROPS: [&str; 8] = ["Red", "Green", "Blue", "Alpha", "Luminance", "Lightness", "Hue", "Saturation"];
|
||||
|
||||
fn smoothie(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let flow = layer_or_self(ctx, &b, "flowLayer", true, false);
|
||||
let prop = ctx.params.e("property");
|
||||
let mut pl = Plane::from_image(&flow, |p| {
|
||||
let (c, a) = unpremul(p);
|
||||
match prop {
|
||||
0 => c[0],
|
||||
1 => c[1],
|
||||
2 => c[2],
|
||||
3 => a,
|
||||
4 => effectcraft_color::luminance(c[0], c[1], c[2]),
|
||||
5 => rgb_to_hsl(c[0], c[1], c[2]).2,
|
||||
6 => rgb_to_hsl(c[0], c[1], c[2]).0,
|
||||
_ => rgb_to_hsl(c[0], c[1], c[2]).1,
|
||||
}
|
||||
});
|
||||
let sm = ctx.params.f("smoothness") * b.scale * 0.5;
|
||||
if sm > 0.05 {
|
||||
pl = gauss_plane(&pl, sm, sm);
|
||||
}
|
||||
let (ax, ay) = b.to_px(ctx.params.v2("sampleA"));
|
||||
let (bx, by) = b.to_px(ctx.params.v2("sampleB"));
|
||||
// Colour palette sampled from the image along A→B.
|
||||
const N: usize = 256;
|
||||
let pal: Vec<Px> = (0..N)
|
||||
.map(|i| {
|
||||
let t = i as f64 / (N - 1) as f64;
|
||||
let p = b.img.sample_bilinear_clamped(ax + (bx - ax) * t, ay + (by - ay) * t);
|
||||
let (c, _) = unpremul(p);
|
||||
[c[0], c[1], c[2], 1.0]
|
||||
})
|
||||
.collect();
|
||||
let phase = (ctx.params.f("phase") / 360.0) as f32;
|
||||
let loops = (ctx.params.e("colorLoop") + 1) as f32;
|
||||
b.img.data.par_iter_mut().zip(pl.data.par_iter()).for_each(|(px, &v)| {
|
||||
if px[3] <= 0.0 {
|
||||
return;
|
||||
}
|
||||
let t = (v * loops + phase).rem_euclid(1.0);
|
||||
// Ping-pong so the palette loops without seams.
|
||||
let t = if t < 0.5 { t * 2.0 } else { 2.0 - t * 2.0 };
|
||||
let f = t * (N - 1) as f32;
|
||||
let i = (f as usize).min(N - 2);
|
||||
let c = lerp4(pal[i], pal[i + 1], f - i as f32);
|
||||
*px = premul([c[0], c[1], c[2]], px[3]);
|
||||
});
|
||||
b
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ specs
|
||||
|
||||
fn light_params() -> Vec<crate::ParamSpec> {
|
||||
vec![
|
||||
p("using", "Using", Value::Enum(0), popup(&["Effect Light", "AE Lights"])),
|
||||
p("lightIntensity", "Light Intensity", num(100.0), slider(0.0, 400.0, 0.0, 200.0, 1)),
|
||||
p("lightColor", "Light Color", col(1.0, 1.0, 1.0), ParamUi::Color),
|
||||
p("lightType", "Light Type", Value::Enum(0), popup(&["Distant Light", "Point Light"])),
|
||||
p("lightHeight", "Light Height", num(50.0), slider(-100.0, 100.0, -100.0, 100.0, 1)),
|
||||
p("lightPosition", "Light Position", pt(0.25, 0.25), ParamUi::Point),
|
||||
p("lightDirection", "Light Direction", num(-45.0), ParamUi::Angle),
|
||||
]
|
||||
}
|
||||
|
||||
fn shading_params(ambient: f64, diffuse: f64, specular: f64, roughness: f64, metal: f64) -> Vec<crate::ParamSpec> {
|
||||
vec![
|
||||
p("ambient", "Ambient", num(ambient), slider(0.0, 200.0, 0.0, 100.0, 1)),
|
||||
p("diffuse", "Diffuse", num(diffuse), slider(0.0, 200.0, 0.0, 100.0, 1)),
|
||||
p("specular", "Specular", num(specular), slider(0.0, 200.0, 0.0, 100.0, 1)),
|
||||
p("roughness", "Roughness", num(roughness), slider(0.001, 0.5, 0.001, 0.5, 3)),
|
||||
p("metal", "Metal", num(metal), pct()),
|
||||
]
|
||||
}
|
||||
|
||||
pub fn specs() -> Vec<EffectSpec> {
|
||||
Vec::new()
|
||||
let mut glass_p = vec![
|
||||
p("bumpMap", "Bump Map", Value::Layer(None), ParamUi::Layer),
|
||||
p("property", "Property", Value::Enum(3), popup(&BUMP_PROPS)),
|
||||
p("softness", "Softness", num(10.0), slider(0.0, 200.0, 0.0, 100.0, 1)),
|
||||
p("height", "Height", num(50.0), slider(-100.0, 100.0, -100.0, 100.0, 1)),
|
||||
p("displacement", "Displacement", num(100.0), slider(-500.0, 500.0, -200.0, 200.0, 1)),
|
||||
];
|
||||
glass_p.extend(light_params());
|
||||
glass_p.extend(shading_params(75.0, 32.0, 26.0, 0.05, 100.0));
|
||||
let mut plastic_p = vec![
|
||||
p("bumpLayer", "Bump Layer", Value::Layer(None), ParamUi::Layer),
|
||||
p("property", "Property", Value::Enum(3), popup(&BUMP_PROPS)),
|
||||
p("softness", "Softness", num(10.0), slider(0.0, 200.0, 0.0, 100.0, 1)),
|
||||
p("height", "Height", num(25.0), slider(-100.0, 100.0, -100.0, 100.0, 1)),
|
||||
p("cutMin", "Cut Min", num(0.0), pct()),
|
||||
p("cutMax", "Cut Max", num(100.0), pct()),
|
||||
];
|
||||
plastic_p.extend(light_params());
|
||||
plastic_p.extend(shading_params(100.0, 70.0, 50.0, 0.05, 50.0));
|
||||
vec![
|
||||
spec(
|
||||
"ec.stylize.ccblockload",
|
||||
"CC Block Load",
|
||||
vec![
|
||||
p("completion", "Completion", num(0.0), pct()),
|
||||
p("scanlines", "Scanlines", Value::Bool(true), ParamUi::Checkbox),
|
||||
p("smoothing", "Smoothing", Value::Bool(true), ParamUi::Checkbox),
|
||||
p("startCleared", "Start Cleared", Value::Bool(true), ParamUi::Checkbox),
|
||||
p("bilinear", "Bilinear", Value::Bool(false), ParamUi::Checkbox),
|
||||
],
|
||||
block_load,
|
||||
),
|
||||
spec(
|
||||
"ec.stylize.ccburnfilm",
|
||||
"CC Burn Film",
|
||||
vec![
|
||||
p("burn", "Burn", num(0.0), pct()),
|
||||
p("center", "Center", pt(0.5, 0.5), ParamUi::Point),
|
||||
p("randomSeed", "Random Seed", num(0.0), slider(0.0, 10000.0, 0.0, 100.0, 0)),
|
||||
],
|
||||
burn_film,
|
||||
),
|
||||
spec("ec.stylize.ccglass", "CC Glass", glass_p, glass),
|
||||
spec(
|
||||
"ec.stylize.cchextile",
|
||||
"CC HexTile",
|
||||
vec![
|
||||
p("render", "Render", Value::Enum(0), popup(&["Normal", "Mirrored", "Rotated"])),
|
||||
p("radius", "Radius", num(50.0), slider(1.0, 4000.0, 2.0, 500.0, 1)),
|
||||
p("center", "Center", pt(0.5, 0.5), ParamUi::Point),
|
||||
p("rotate", "Rotate", num(0.0), ParamUi::Angle),
|
||||
p("smearing", "Smearing", num(0.0), pct()),
|
||||
],
|
||||
hextile,
|
||||
),
|
||||
spec(
|
||||
"ec.stylize.ccmrsmoothie",
|
||||
"CC Mr. Smoothie",
|
||||
vec![
|
||||
p("flowLayer", "Flow Layer", Value::Layer(None), ParamUi::Layer),
|
||||
p("property", "Property", Value::Enum(4), popup(&SMOOTHIE_PROPS)),
|
||||
p("smoothness", "Smoothness", num(20.0), slider(0.0, 200.0, 0.0, 100.0, 1)),
|
||||
p("sampleA", "Sample A", pt(0.25, 0.5), ParamUi::Point),
|
||||
p("sampleB", "Sample B", pt(0.75, 0.5), ParamUi::Point),
|
||||
p("phase", "Phase", num(0.0), ParamUi::Angle),
|
||||
p("colorLoop", "Color Loop", Value::Enum(0), popup(&["1x", "2x", "3x", "4x"])),
|
||||
],
|
||||
smoothie,
|
||||
),
|
||||
spec("ec.stylize.ccplastic", "CC Plastic", plastic_p, plastic),
|
||||
]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{EffectEnv, run_fx};
|
||||
|
||||
fn ramp(w: u32, h: u32) -> Image {
|
||||
let mut img = Image::new(w, h);
|
||||
for y in 0..h {
|
||||
for x in 0..w {
|
||||
img.set(x, y, [x as f32 / w as f32, y as f32 / h as f32, 0.3, 1.0]);
|
||||
}
|
||||
}
|
||||
img
|
||||
}
|
||||
|
||||
fn bumpy(w: u32, h: u32) -> Image {
|
||||
let mut img = ramp(w, h);
|
||||
for y in 0..h {
|
||||
for x in 0..w {
|
||||
let a = if (x as i32 - 20).pow(2) + (y as i32 - 16).pow(2) < 64 { 1.0 } else { 0.5 };
|
||||
let p = img.data[(y * w + x) as usize];
|
||||
img.set(x, y, [p[0] * a, p[1] * a, p[2] * a, a]);
|
||||
}
|
||||
}
|
||||
img
|
||||
}
|
||||
|
||||
fn run(id: &str, vals: &[(&str, Value)], img: Image) -> Buf {
|
||||
run_fx(id, vals, img, 0.0, EffectEnv::default())
|
||||
}
|
||||
|
||||
fn max_diff(a: &Image, b: &Image) -> f32 {
|
||||
a.data.iter().zip(&b.data).flat_map(|(p, q)| (0..4).map(move |c| (p[c] - q[c]).abs())).fold(0.0, f32::max)
|
||||
}
|
||||
|
||||
const ALL: [&str; 6] =
|
||||
["ec.stylize.ccblockload", "ec.stylize.ccburnfilm", "ec.stylize.ccglass", "ec.stylize.cchextile", "ec.stylize.ccmrsmoothie", "ec.stylize.ccplastic"];
|
||||
|
||||
#[test]
|
||||
fn all_deterministic() {
|
||||
for id in ALL {
|
||||
let vals: &[(&str, Value)] = match id {
|
||||
"ec.stylize.ccblockload" => &[("completion", num(40.0))],
|
||||
"ec.stylize.ccburnfilm" => &[("burn", num(50.0))],
|
||||
_ => &[],
|
||||
};
|
||||
let a = run(id, vals, bumpy(40, 32));
|
||||
let b = run(id, vals, bumpy(40, 32));
|
||||
assert_eq!(a.img.data, b.img.data, "{id}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_load_complete_is_identity_and_start_cleared() {
|
||||
let img = ramp(40, 30);
|
||||
assert_eq!(run("ec.stylize.ccblockload", &[("completion", num(100.0))], img.clone()).img.data, img.data);
|
||||
let o = run("ec.stylize.ccblockload", &[("completion", num(0.0))], img.clone());
|
||||
assert!(o.img.data.iter().all(|p| p[3] == 0.0));
|
||||
// Midway: blocky (fewer distinct colours than the source).
|
||||
let o = run("ec.stylize.ccblockload", &[("completion", num(50.0)), ("smoothing", Value::Bool(false))], img.clone());
|
||||
let mut cols: Vec<u32> = o.img.data.iter().map(|p| (p[0] * 1000.0) as u32).collect();
|
||||
cols.sort();
|
||||
cols.dedup();
|
||||
assert!(cols.len() < 40, "{}", cols.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn burn_film_zero_identity_full_burns() {
|
||||
let img = ramp(40, 30);
|
||||
assert_eq!(run("ec.stylize.ccburnfilm", &[], img.clone()).img.data, img.data);
|
||||
let o = run("ec.stylize.ccburnfilm", &[("burn", num(100.0))], img);
|
||||
assert!(o.img.data.iter().all(|p| p[3] == 0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn glass_flat_without_highlight_is_identity() {
|
||||
let img = bumpy(40, 32);
|
||||
let o = run("ec.stylize.ccglass", &[("height", num(0.0)), ("specular", num(0.0))], img.clone());
|
||||
assert!(max_diff(&o.img, &img) < 1e-4, "{}", max_diff(&o.img, &img));
|
||||
let o = run("ec.stylize.ccglass", &[], img.clone());
|
||||
assert!(max_diff(&o.img, &img) > 0.01);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plastic_shades_bumps() {
|
||||
let img = bumpy(40, 32);
|
||||
let o = run("ec.stylize.ccplastic", &[("height", num(0.0)), ("specular", num(0.0))], img.clone());
|
||||
assert!(max_diff(&o.img, &img) < 1e-4);
|
||||
let o = run("ec.stylize.ccplastic", &[("height", num(80.0))], img.clone());
|
||||
assert!(max_diff(&o.img, &img) > 0.01);
|
||||
// Alpha is untouched.
|
||||
assert!(o.img.data.iter().zip(&img.data).all(|(a, b)| (a[3] - b[3]).abs() < 1e-6));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hextile_repeats_centre() {
|
||||
let img = ramp(64, 64);
|
||||
let o = run("ec.stylize.cchextile", &[("radius", num(8.0)), ("center", pt(32.0, 32.0))], img.clone());
|
||||
assert!(max_diff(&o.img, &img) > 0.1);
|
||||
// Pixels at the centre are unchanged (inside the central tile).
|
||||
let c = (32 * 64 + 32) as usize;
|
||||
assert!((0..4).all(|i| (o.img.data[c][i] - img.data[c][i]).abs() < 1e-4));
|
||||
// Full smearing restores the source.
|
||||
let o = run("ec.stylize.cchextile", &[("radius", num(8.0)), ("smearing", num(100.0))], img.clone());
|
||||
assert!(max_diff(&o.img, &img) < 1e-4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn smoothie_maps_to_sampled_palette() {
|
||||
let img = ramp(40, 30);
|
||||
let o = run("ec.stylize.ccmrsmoothie", &[("smoothness", num(0.0)), ("sampleA", pt(10.0, 15.0)), ("sampleB", pt(30.0, 15.0))], img.clone());
|
||||
// Every output colour lies on the palette line between A and B (blue stays 0.3, green ≈ 0.5).
|
||||
assert!(o.img.data.iter().all(|p| (p[2] - 0.3).abs() < 1e-3 && (p[1] - 0.5).abs() < 0.03));
|
||||
assert!(max_diff(&o.img, &img) > 0.1);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue