mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-14 19:27:05 -04:00
M9.13: Card Dance / Shatter / Card Wipe camera systems, lighting and material
- card3d: shared Camera System (Camera Position with rotations, X/Y position and Transform Order; Corner Pins with focal length; Comp Camera via EffectHost::comp_scene), Lighting (Distant / Point / First Comp Light, colour, position, depth, ambient) and Material (diffuse, specular, highlight sharpness). - Shatter: Custom Shatter Map (+ White Tiles Fixed), Gradient group, Textures group (front/side/back modes and layers, colour, opacity) and extrusion side rendering. - Card Wipe: true 3D cards, Back Layer as a layer parameter (old None/Self popup migrated), Flip Direction, Position / Rotation Jitter. - Renderer supplies the comp camera and first light to effects. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
396268496b
commit
0ed5c87b94
10 files changed
+1118
-145
No files matched your search
@@ -0,0 +1,413 @@
|
||||
//! The 3D system shared by Card Dance, Shatter and Card Wipe: Camera System (Camera Position,
|
||||
//! Corner Pins, Comp Camera), Lighting (Distant Source, Point Source, First Comp Light) and
|
||||
//! Material (diffuse, specular, highlight sharpness).
|
||||
//!
|
||||
//! Pieces live in "buffer world" space: x, y in buffer pixels, z in buffer pixels away from the
|
||||
//! viewer (the layer plane is z = 0). A [`Proj`] maps buffer-world points to buffer pixels
|
||||
//! (homogeneous), so every camera system reduces to one 3×4 matrix.
|
||||
|
||||
use effectcraft_keyframe::Value;
|
||||
use effectcraft_project::ParamUi;
|
||||
|
||||
use crate::sim::pt;
|
||||
use crate::{Buf, EffectCtx, ParamSpec, col, num, p, popup, slider};
|
||||
|
||||
pub(crate) type M3 = [[f64; 3]; 3];
|
||||
|
||||
/// Camera System options.
|
||||
pub const CAMERA_SYSTEMS: [&str; 3] = ["Camera Position", "Corner Pins", "Comp Camera"];
|
||||
/// Transform Order options (rotation order, and whether the camera position applies before or
|
||||
/// after the rotation).
|
||||
pub const TRANSFORM_ORDERS: [&str; 12] = [
|
||||
"Rotate XYZ, Position",
|
||||
"Rotate XZY, Position",
|
||||
"Rotate YXZ, Position",
|
||||
"Rotate YZX, Position",
|
||||
"Rotate ZXY, Position",
|
||||
"Rotate ZYX, Position",
|
||||
"Position, Rotate XYZ",
|
||||
"Position, Rotate XZY",
|
||||
"Position, Rotate YXZ",
|
||||
"Position, Rotate YZX",
|
||||
"Position, Rotate ZXY",
|
||||
"Position, Rotate ZYX",
|
||||
];
|
||||
/// Light Type options.
|
||||
pub const LIGHT_TYPES: [&str; 3] = ["Distant Source", "Point Source", "First Comp Light"];
|
||||
|
||||
/// Camera System, Camera Position and Corner Pins parameters. `z` is the default camera
|
||||
/// distance (Z Position, in layer sizes at the 70 mm-equivalent Focal Length).
|
||||
pub(crate) fn camera_params(z: f64) -> Vec<ParamSpec> {
|
||||
vec![
|
||||
p("cameraSystem", "Camera System", Value::Enum(0), popup(&CAMERA_SYSTEMS)),
|
||||
p("cameraPosition/xRotation", "X Rotation", num(0.0), ParamUi::Angle),
|
||||
p("cameraPosition/yRotation", "Y Rotation", num(0.0), ParamUi::Angle),
|
||||
p("cameraPosition/zRotation", "Z Rotation", num(0.0), ParamUi::Angle),
|
||||
p("cameraPosition/xyPosition", "X, Y Position", pt(0.5, 0.5), ParamUi::Point),
|
||||
p("cameraPosition/cameraZ", "Z Position", num(z), slider(0.05, 10.0, 0.5, 5.0, 2)),
|
||||
p("cameraPosition/focalLength", "Focal Length", num(70.0), slider(1.0, 500.0, 10.0, 200.0, 1)),
|
||||
p("cameraPosition/transformOrder", "Transform Order", Value::Enum(0), popup(&TRANSFORM_ORDERS)),
|
||||
p("cornerPins/upperLeftCorner", "Upper Left Corner", pt(0.0, 0.0), ParamUi::Point),
|
||||
p("cornerPins/upperRightCorner", "Upper Right Corner", pt(1.0, 0.0), ParamUi::Point),
|
||||
p("cornerPins/lowerLeftCorner", "Lower Left Corner", pt(0.0, 1.0), ParamUi::Point),
|
||||
p("cornerPins/lowerRightCorner", "Lower Right Corner", pt(1.0, 1.0), ParamUi::Point),
|
||||
p("cornerPins/autoFocalLength", "Auto Focal Length", Value::Bool(true), ParamUi::Checkbox),
|
||||
p("cornerPins/focalLength", "Focal Length", num(70.0), slider(1.0, 500.0, 10.0, 200.0, 1)),
|
||||
]
|
||||
}
|
||||
|
||||
/// Lighting parameters.
|
||||
pub(crate) fn lighting_params(intensity: f64, ambient: f64) -> Vec<ParamSpec> {
|
||||
vec![
|
||||
p("lighting/lightType", "Light Type", Value::Enum(0), popup(&LIGHT_TYPES)),
|
||||
p("lighting/lightIntensity", "Light Intensity", num(intensity), slider(0.0, 4.0, 0.0, 2.0, 2)),
|
||||
p("lighting/lightColor", "Light Color", col(1.0, 1.0, 1.0), ParamUi::Color),
|
||||
p("lighting/lightPosition", "Light Position", pt(0.5, 0.5), ParamUi::Point),
|
||||
p("lighting/lightDepth", "Light Depth", num(1.0), slider(-10.0, 10.0, 0.0, 5.0, 3)),
|
||||
p("lighting/ambientLight", "Ambient Light", num(ambient), slider(0.0, 2.0, 0.0, 1.0, 2)),
|
||||
]
|
||||
}
|
||||
|
||||
/// Material parameters. Specular defaults to 0 so pieces at rest reproduce the layer.
|
||||
pub(crate) fn material_params(diffuse: f64) -> Vec<ParamSpec> {
|
||||
vec![
|
||||
p("material/diffuse", "Diffuse Reflection", num(diffuse), slider(0.0, 2.0, 0.0, 1.0, 2)),
|
||||
p("material/specular", "Specular Reflection", num(0.0), slider(0.0, 2.0, 0.0, 1.0, 2)),
|
||||
p("material/highlightSharpness", "Highlight Sharpness", num(15.0), slider(1.0, 100.0, 1.0, 100.0, 1)),
|
||||
]
|
||||
}
|
||||
|
||||
/// Whether the camera / lighting parameter `param` is shown for the current choices.
|
||||
pub(crate) fn shown(param: &str, e: &dyn Fn(&str) -> u32) -> Option<bool> {
|
||||
if param == "cameraPosition" || param.starts_with("cameraPosition/") {
|
||||
return Some(e("cameraSystem") == 0);
|
||||
}
|
||||
if param == "cornerPins" || param.starts_with("cornerPins/") {
|
||||
return Some(e("cameraSystem") == 1);
|
||||
}
|
||||
if param == "lighting/lightPosition" || param == "lighting/lightDepth" {
|
||||
return Some(e("lighting/lightType") != 2);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) fn mat_mul(a: &M3, b: &M3) -> M3 {
|
||||
std::array::from_fn(|i| std::array::from_fn(|j| (0..3).map(|k| a[i][k] * b[k][j]).sum()))
|
||||
}
|
||||
|
||||
fn rx(a: f64) -> M3 {
|
||||
let (s, c) = a.sin_cos();
|
||||
[[1.0, 0.0, 0.0], [0.0, c, -s], [0.0, s, c]]
|
||||
}
|
||||
fn ry(a: f64) -> M3 {
|
||||
let (s, c) = a.sin_cos();
|
||||
[[c, 0.0, s], [0.0, 1.0, 0.0], [-s, 0.0, c]]
|
||||
}
|
||||
fn rz(a: f64) -> M3 {
|
||||
let (s, c) = a.sin_cos();
|
||||
[[c, -s, 0.0], [s, c, 0.0], [0.0, 0.0, 1.0]]
|
||||
}
|
||||
|
||||
/// Rotation applying the axes in `order` (0 XYZ, 1 XZY, 2 YXZ, 3 YZX, 4 ZXY, 5 ZYX; the first
|
||||
/// letter is applied first). Angles in radians.
|
||||
pub(crate) fn rot_ordered(x: f64, y: f64, z: f64, order: u32) -> M3 {
|
||||
let (mx, my, mz) = (rx(x), ry(y), rz(z));
|
||||
let seq: [&M3; 3] = match order % 6 {
|
||||
1 => [&mx, &mz, &my],
|
||||
2 => [&my, &mx, &mz],
|
||||
3 => [&my, &mz, &mx],
|
||||
4 => [&mz, &mx, &my],
|
||||
5 => [&mz, &my, &mx],
|
||||
_ => [&mx, &my, &mz],
|
||||
};
|
||||
mat_mul(seq[2], &mat_mul(seq[1], seq[0]))
|
||||
}
|
||||
|
||||
/// A camera: buffer-world → buffer pixels (homogeneous rows X, Y, W; W is the depth in front
|
||||
/// of the camera, in the units `near` uses).
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(crate) struct Proj {
|
||||
pub m: [[f64; 4]; 3],
|
||||
/// Points with W below this are behind / too close to the camera.
|
||||
pub near: f64,
|
||||
}
|
||||
|
||||
impl Proj {
|
||||
/// Looking down +z at the layer plane from `d` pixels in front of buffer point `c`.
|
||||
pub fn simple(c: [f64; 2], d: f64) -> Proj {
|
||||
Proj { m: [[d, 0.0, c[0], 0.0], [0.0, d, c[1], 0.0], [0.0, 0.0, 1.0, d]], near: 0.05 * d }
|
||||
}
|
||||
|
||||
/// `self` after the affine scene transform `p ↦ a·p + t`.
|
||||
fn after(&self, a: &M3, t: [f64; 3]) -> Proj {
|
||||
let mut m = [[0.0; 4]; 3];
|
||||
for i in 0..3 {
|
||||
for j in 0..3 {
|
||||
m[i][j] = (0..3).map(|k| self.m[i][k] * a[k][j]).sum();
|
||||
}
|
||||
m[i][3] = (0..3).map(|k| self.m[i][k] * t[k]).sum::<f64>() + self.m[i][3];
|
||||
}
|
||||
Proj { m, near: self.near }
|
||||
}
|
||||
|
||||
/// A screen-space homography applied after `self`.
|
||||
fn then(&self, h: &M3) -> Proj {
|
||||
let mut m = [[0.0; 4]; 3];
|
||||
for i in 0..3 {
|
||||
for j in 0..4 {
|
||||
m[i][j] = (0..3).map(|k| h[i][k] * self.m[k][j]).sum();
|
||||
}
|
||||
}
|
||||
// Keep W's scale (and so `near`) comparable when H rescales it.
|
||||
Proj { m, near: self.near * h[2][2].abs().max(1e-9) }
|
||||
}
|
||||
|
||||
/// Project a buffer-world point (None behind the camera).
|
||||
#[cfg(test)]
|
||||
pub fn project(&self, p: [f64; 3]) -> Option<[f64; 2]> {
|
||||
let r = |i: usize| self.m[i][0] * p[0] + self.m[i][1] * p[1] + self.m[i][2] * p[2] + self.m[i][3];
|
||||
let w = r(2);
|
||||
(w > self.near).then(|| [r(0) / w, r(1) / w])
|
||||
}
|
||||
|
||||
/// The camera's centre in buffer-world space (where the projection collapses), or a point
|
||||
/// far in front of the layer when it has none (orthographic).
|
||||
pub fn eye(&self) -> [f64; 3] {
|
||||
let a: M3 = std::array::from_fn(|i| [self.m[i][0], self.m[i][1], self.m[i][2]]);
|
||||
match inv3(&a) {
|
||||
Some(ai) => {
|
||||
let t = [-self.m[0][3], -self.m[1][3], -self.m[2][3]];
|
||||
std::array::from_fn(|i| (0..3).map(|k| ai[i][k] * t[k]).sum())
|
||||
}
|
||||
None => [0.0, 0.0, -1e9],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn inv3(m: &M3) -> Option<M3> {
|
||||
let det = m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1]) - m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0])
|
||||
+ m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0]);
|
||||
if det.abs() < 1e-12 {
|
||||
return None;
|
||||
}
|
||||
let d = 1.0 / det;
|
||||
Some([
|
||||
[(m[1][1] * m[2][2] - m[1][2] * m[2][1]) * d, (m[0][2] * m[2][1] - m[0][1] * m[2][2]) * d, (m[0][1] * m[1][2] - m[0][2] * m[1][1]) * d],
|
||||
[(m[1][2] * m[2][0] - m[1][0] * m[2][2]) * d, (m[0][0] * m[2][2] - m[0][2] * m[2][0]) * d, (m[0][2] * m[1][0] - m[0][0] * m[1][2]) * d],
|
||||
[(m[1][0] * m[2][1] - m[1][1] * m[2][0]) * d, (m[0][1] * m[2][0] - m[0][0] * m[2][1]) * d, (m[0][0] * m[1][1] - m[0][1] * m[1][0]) * d],
|
||||
])
|
||||
}
|
||||
|
||||
/// Homography taking the unit square's corners (0,0) (1,0) (1,1) (0,1) to `q`.
|
||||
fn square_to_quad(q: [[f64; 2]; 4]) -> M3 {
|
||||
let v = |p: [f64; 2]| effectcraft_geom::Vec2 { x: p[0], y: p[1] };
|
||||
effectcraft_geom::Mat3::square_to_quad([v(q[0]), v(q[1]), v(q[2]), v(q[3])]).0
|
||||
}
|
||||
|
||||
/// The camera an effect renders through, from its Camera System parameters.
|
||||
pub(crate) fn projection(ctx: &EffectCtx, b: &Buf) -> Proj {
|
||||
let pr = ctx.params;
|
||||
let [lw, lh] = ctx.layer_size;
|
||||
let s = b.scale;
|
||||
let c = [b.offset[0] + lw * 0.5 * s, b.offset[1] + lh * 0.5 * s];
|
||||
let size = lw.max(lh) * s;
|
||||
let f = |id: &str, d: f64| pr.get(id).map(Value::as_f64).unwrap_or(d);
|
||||
match pr.e("cameraSystem") {
|
||||
1 => {
|
||||
// Corner Pins: the layer's corners land on the four pins; the focal length sets how
|
||||
// strongly depth (pieces leaving the plane) is foreshortened.
|
||||
let focal = if pr.get("cornerPins/autoFocalLength").is_none_or(|v| v.as_bool()) { 70.0 } else { f("cornerPins/focalLength", 70.0).max(1.0) };
|
||||
let base = Proj::simple(c, size * 2.0 * focal / 70.0);
|
||||
let pin = |id: &str| {
|
||||
let (x, y) = b.to_px(pr.v2(id));
|
||||
[x, y]
|
||||
};
|
||||
let quad =
|
||||
[pin("cornerPins/upperLeftCorner"), pin("cornerPins/upperRightCorner"), pin("cornerPins/lowerRightCorner"), pin("cornerPins/lowerLeftCorner")];
|
||||
let rect = [
|
||||
[b.offset[0], b.offset[1]],
|
||||
[b.offset[0] + lw * s, b.offset[1]],
|
||||
[b.offset[0] + lw * s, b.offset[1] + lh * s],
|
||||
[b.offset[0], b.offset[1] + lh * s],
|
||||
];
|
||||
match inv3(&square_to_quad(rect)) {
|
||||
Some(ri) => base.then(&mat_mul(&square_to_quad(quad), &ri)),
|
||||
None => base,
|
||||
}
|
||||
}
|
||||
2 => {
|
||||
// Comp Camera: the comp's camera looking at this layer (layer px → layer px).
|
||||
let base = Proj::simple(c, size * 2.0);
|
||||
let Some(cam) = ctx.env.host.and_then(|h| h.comp_scene()).and_then(|sc| sc.camera) else { return base };
|
||||
// buffer world → layer: (p − offset) / s; layer → buffer: · s + offset.
|
||||
let mut m = [[0.0; 4]; 3];
|
||||
for i in 0..3 {
|
||||
for j in 0..3 {
|
||||
m[i][j] = cam[i][j] / s;
|
||||
}
|
||||
m[i][3] = cam[i][3] - (cam[i][0] * b.offset[0] + cam[i][1] * b.offset[1]) / s;
|
||||
}
|
||||
let back: M3 = [[s, 0.0, b.offset[0]], [0.0, s, b.offset[1]], [0.0, 0.0, 1.0]];
|
||||
let p = Proj { m, near: 1e-6 }.then(&back);
|
||||
// W of the layer centre sets the scale of `near`.
|
||||
let wc = p.m[2][0] * c[0] + p.m[2][1] * c[1] + p.m[2][3];
|
||||
Proj { near: (wc.abs() * 0.02).max(1e-9), ..p }
|
||||
}
|
||||
_ => {
|
||||
let zpos = f("cameraPosition/cameraZ", 2.0).max(0.05);
|
||||
let focal = f("cameraPosition/focalLength", 70.0).max(1.0);
|
||||
let base = Proj::simple(c, size * zpos * focal / 140.0);
|
||||
let order = pr.e("cameraPosition/transformOrder");
|
||||
let r = rot_ordered(
|
||||
f("cameraPosition/xRotation", 0.0).to_radians(),
|
||||
f("cameraPosition/yRotation", 0.0).to_radians(),
|
||||
f("cameraPosition/zRotation", 0.0).to_radians(),
|
||||
order,
|
||||
);
|
||||
let xy = pr.get("cameraPosition/xyPosition").map(|v| v.as_vec2()).unwrap_or([lw * 0.5, lh * 0.5]);
|
||||
let (px, py) = b.to_px(xy);
|
||||
let shift = [px - c[0], py - c[1], 0.0];
|
||||
// Rotate about the layer centre, then move by the X, Y Position offset (or the
|
||||
// other way round).
|
||||
let c3 = [c[0], c[1], 0.0];
|
||||
let rc: [f64; 3] = std::array::from_fn(|i| c3[i] - (0..3).map(|k| r[i][k] * c3[k]).sum::<f64>());
|
||||
let t = if order >= 6 {
|
||||
// p ↦ R(p + shift − c) + c
|
||||
std::array::from_fn(|i| rc[i] + (0..3).map(|k| r[i][k] * shift[k]).sum::<f64>())
|
||||
} else {
|
||||
std::array::from_fn(|i| rc[i] + shift[i])
|
||||
};
|
||||
base.after(&r, t)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The comp's first light as the effect sees it (layer pixels), from [`crate::EffectHost::comp_scene`].
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct CompLight {
|
||||
/// Position in layer pixels (z away from the viewer).
|
||||
pub pos: [f64; 3],
|
||||
/// Direction the light travels (parallel / spot lights), unit, layer space.
|
||||
pub dir: [f64; 3],
|
||||
/// Colour × intensity (1 = 100 %).
|
||||
pub color: [f32; 3],
|
||||
/// 0 parallel, 1 point / spot, 2 ambient.
|
||||
pub kind: u8,
|
||||
}
|
||||
|
||||
/// The comp's camera and lights relative to the effect's layer.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct CompScene {
|
||||
/// Layer pixels (x, y, z) → layer pixels, homogeneous rows X, Y, W: the comp camera's view
|
||||
/// of the layer brought back into the layer's own pixel grid (identity on the layer plane
|
||||
/// for the default camera).
|
||||
pub camera: Option<[[f64; 4]; 3]>,
|
||||
pub light: Option<CompLight>,
|
||||
}
|
||||
|
||||
fn sub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
|
||||
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
|
||||
}
|
||||
fn dot(a: [f64; 3], b: [f64; 3]) -> f64 {
|
||||
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
|
||||
}
|
||||
fn norm(a: [f64; 3]) -> [f64; 3] {
|
||||
let l = dot(a, a).sqrt();
|
||||
if l < 1e-12 { [0.0, 0.0, -1.0] } else { [a[0] / l, a[1] / l, a[2] / l] }
|
||||
}
|
||||
|
||||
/// Lighting and Material, ready to shade pieces.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(crate) struct Lighting {
|
||||
/// 0 distant, 1 point, 2 ambient only.
|
||||
kind: u8,
|
||||
color: [f32; 3],
|
||||
pos: [f64; 3],
|
||||
/// Distant light: direction towards the light.
|
||||
to_light: [f64; 3],
|
||||
ambient: f32,
|
||||
kd: f32,
|
||||
ks: f32,
|
||||
sharp: f32,
|
||||
}
|
||||
|
||||
impl Lighting {
|
||||
pub fn from(ctx: &EffectCtx, b: &Buf) -> Lighting {
|
||||
let pr = ctx.params;
|
||||
let f = |id: &str, d: f64| pr.get(id).map(Value::as_f64).unwrap_or(d);
|
||||
let [lw, lh] = ctx.layer_size;
|
||||
let s = b.scale;
|
||||
let centre = [b.offset[0] + lw * 0.5 * s, b.offset[1] + lh * 0.5 * s, 0.0];
|
||||
let intensity = f("lighting/lightIntensity", 1.0) as f32;
|
||||
let lc = pr.get("lighting/lightColor").map(|v| v.as_color()).unwrap_or([1.0; 4]);
|
||||
let mut color = [lc[0] * intensity, lc[1] * intensity, lc[2] * intensity];
|
||||
let lp = pr.get("lighting/lightPosition").map(|v| v.as_vec2()).unwrap_or([lw * 0.5, lh * 0.5]);
|
||||
let (x, y) = b.to_px(lp);
|
||||
let mut pos = [x, y, -f("lighting/lightDepth", 1.0) * lw.max(lh) * s];
|
||||
let mut kind = if pr.e("lighting/lightType") == 1 { 1 } else { 0 };
|
||||
let mut to_light = norm(sub(pos, centre));
|
||||
if pr.e("lighting/lightType") == 2 {
|
||||
match ctx.env.host.and_then(|h| h.comp_scene()).and_then(|sc| sc.light) {
|
||||
Some(l) => {
|
||||
color = [l.color[0] * intensity, l.color[1] * intensity, l.color[2] * intensity];
|
||||
pos = [l.pos[0] * s + b.offset[0], l.pos[1] * s + b.offset[1], l.pos[2] * s];
|
||||
kind = l.kind;
|
||||
to_light = norm([-l.dir[0], -l.dir[1], -l.dir[2]]);
|
||||
}
|
||||
// No light in the comp: ambient only.
|
||||
None => kind = 2,
|
||||
}
|
||||
}
|
||||
Lighting {
|
||||
kind,
|
||||
color,
|
||||
pos,
|
||||
to_light,
|
||||
ambient: f("lighting/ambientLight", 0.25) as f32,
|
||||
kd: f("material/diffuse", 0.75) as f32,
|
||||
ks: f("material/specular", 0.0) as f32,
|
||||
sharp: f("material/highlightSharpness", 15.0).max(1.0) as f32,
|
||||
}
|
||||
}
|
||||
|
||||
/// Multiplier and additive highlight for a piece with unit normal `n` (rotated front normal)
|
||||
/// at `at`, seen from `eye`. Pieces are lit on whichever side faces the viewer.
|
||||
pub fn shade(&self, n: [f64; 3], at: [f64; 3], eye: [f64; 3]) -> ([f32; 3], [f32; 3]) {
|
||||
let v = norm(sub(eye, at));
|
||||
let n = if dot(n, v) < 0.0 { [-n[0], -n[1], -n[2]] } else { n };
|
||||
if self.kind == 2 {
|
||||
let a = self.ambient;
|
||||
return ([a + self.kd * self.color[0], a + self.kd * self.color[1], a + self.kd * self.color[2]], [0.0; 3]);
|
||||
}
|
||||
let l = if self.kind == 1 { norm(sub(self.pos, at)) } else { self.to_light };
|
||||
let ndl = dot(n, l).max(0.0) as f32;
|
||||
let h = norm([l[0] + v[0], l[1] + v[1], l[2] + v[2]]);
|
||||
let spec = if self.ks > 0.0 && ndl > 0.0 { self.ks * (dot(n, h).max(0.0) as f32).powf(self.sharp) } else { 0.0 };
|
||||
(std::array::from_fn(|k| self.ambient + self.kd * ndl * self.color[k]), std::array::from_fn(|k| spec * self.color[k]))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn simple_camera_keeps_the_layer_plane() {
|
||||
let p = Proj::simple([10.0, 20.0], 100.0);
|
||||
assert_eq!(p.project([3.0, 4.0, 0.0]), Some([3.0, 4.0]));
|
||||
let e = p.eye();
|
||||
assert!((e[0] - 10.0).abs() < 1e-9 && (e[1] - 20.0).abs() < 1e-9 && (e[2] + 100.0).abs() < 1e-9);
|
||||
// Further away looks smaller (towards the centre).
|
||||
let q = p.project([30.0, 20.0, 100.0]).unwrap();
|
||||
assert!((q[0] - 20.0).abs() < 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rotation_orders_differ() {
|
||||
let a = rot_ordered(0.5, 0.3, 0.2, 0);
|
||||
let b = rot_ordered(0.5, 0.3, 0.2, 5);
|
||||
assert!((0..3).any(|i| (0..3).any(|j| (a[i][j] - b[i][j]).abs() > 1e-3)));
|
||||
let z = rot_ordered(0.0, 0.0, 0.0, 3);
|
||||
assert_eq!(z, [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]);
|
||||
}
|
||||
}
|
||||
@@ -35,15 +35,12 @@ pub const PARTIAL: &[(&str, &str)] = &[
|
||||
("ec.noise.removegrain", "no preview region, sampling or temporal filtering"),
|
||||
("ec.vr.digitalglitch", "reduced control set"),
|
||||
("ec.vr.converter", "common layouts only"),
|
||||
("ec.sim.carddance", "no camera system, corner pins or lighting/material controls"),
|
||||
("ec.sim.shatter", "no custom shatter map, gradient, textures or extrusion rendering"),
|
||||
("ec.sim.caustics", "no Sky group or light type"),
|
||||
("ec.sim.foam", "no wobble, pop velocity, custom bubble texture, environment map or flow map"),
|
||||
("ec.sim.particleplayground", "no Particle Exploder, Ephemeral Property Mapper, Affects groups or text particles"),
|
||||
("ec.sim.waveworld", "no wireframe controls, dry-area rendering or ground group"),
|
||||
("ec.stylize.glow", "no Glow Operation or arbitrary colour map"),
|
||||
("ec.stylize.cartoon", "no Edge Enhancement"),
|
||||
("ec.transition.cardwipe", "2D flip only; no back layer picker, camera, lighting or jitter"),
|
||||
("ec.transition.blockdissolve", "no Soft Edges option"),
|
||||
];
|
||||
|
||||
@@ -79,6 +76,11 @@ pub fn param_shown(effect: &str, param: &str, value: &dyn Fn(&str) -> Option<eff
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
"ec.sim.carddance" | "ec.transition.cardwipe" => crate::card3d::shown(param, &e).unwrap_or(true),
|
||||
"ec.sim.shatter" => match param {
|
||||
"shape/customShatterMap" | "shape/whiteTilesFixed" => e("shape/pattern") == 5,
|
||||
_ => crate::card3d::shown(param, &e).unwrap_or(true),
|
||||
},
|
||||
_ => {
|
||||
let _ = b("");
|
||||
true
|
||||
|
||||
@@ -10,6 +10,7 @@ pub mod audio_fx;
|
||||
mod blur2;
|
||||
mod blur3;
|
||||
pub mod camera_tracker;
|
||||
mod card3d;
|
||||
pub mod catalog;
|
||||
mod channel;
|
||||
mod channel2;
|
||||
@@ -60,6 +61,7 @@ pub mod warp_stab;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
pub use card3d::{CompLight, CompScene};
|
||||
pub use color_fx::{HUESAT_CHANNELS, LEVELS_CHANNELS, exposure_settings, huesat_ranges_identity, levels_channel_ids, levels_channels_identity, levels_clip};
|
||||
pub use color2::Curve;
|
||||
use effectcraft_keyframe::Value;
|
||||
@@ -260,6 +262,11 @@ pub trait EffectHost: Sync {
|
||||
fn aux(&self) -> Option<std::sync::Arc<AuxChannels>> {
|
||||
None
|
||||
}
|
||||
/// The composition's camera and first light relative to the effect's layer (Card
|
||||
/// Dance / Shatter / Card Wipe's Comp Camera and First Comp Light). `None` when unknown.
|
||||
fn comp_scene(&self) -> Option<CompScene> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Extra context the renderer may supply (all optional; `Default` is "nothing known").
|
||||
@@ -573,6 +580,11 @@ pub const PARAM_GROUPS: &[(&str, &str)] = &[
|
||||
("xScale", "X Scale"),
|
||||
("yScale", "Y Scale"),
|
||||
("cameraPosition", "Camera Position"),
|
||||
("cornerPins", "Corner Pins"),
|
||||
("positionJitter", "Position Jitter"),
|
||||
("rotationJitter", "Rotation Jitter"),
|
||||
("textures", "Textures"),
|
||||
("gradient", "Gradient"),
|
||||
("cannon", "Cannon"),
|
||||
("grid", "Grid"),
|
||||
("layerExploder", "Layer Exploder"),
|
||||
@@ -677,6 +689,8 @@ pub const TIME_DEPENDENT: &[&str] = &[
|
||||
"ec.time.ccforcemotionblur",
|
||||
"ec.time.ccwidetime",
|
||||
"ec.time.pixelmotionblur",
|
||||
// Card Wipe's position / rotation jitter moves with time.
|
||||
"ec.transition.cardwipe",
|
||||
// Temporal Smoothing reads neighbouring frames.
|
||||
"ec.color.autolevels",
|
||||
"ec.color.autocontrast",
|
||||
@@ -711,7 +725,8 @@ pub fn apply(spec: &EffectSpec, ctx: &EffectCtx, buf: Buf) -> Buf {
|
||||
#[cfg(test)]
|
||||
pub(crate) fn run_fx(id: &str, vals: &[(&str, Value)], img: Image, time: f64, env: EffectEnv) -> Buf {
|
||||
let s = find(id).unwrap_or_else(|| panic!("no effect {id}"));
|
||||
let mut params = Params { values: s.params.iter().map(|p| (p.id.to_string(), p.default.clone())).collect() };
|
||||
let size = [img.width as f64, img.height as f64];
|
||||
let mut params = Params { values: s.params.iter().map(|p| (p.id.to_string(), default_value(p, size))).collect() };
|
||||
for (k, v) in vals {
|
||||
assert!(params.values.contains_key(*k), "{id}: unknown param {k}");
|
||||
params.values.insert(k.to_string(), v.clone());
|
||||
|
||||
@@ -242,6 +242,18 @@ fn is_numeric_ui(u: &ParamUi) -> bool {
|
||||
/// point defaults of added parameters. Returns whether anything changed.
|
||||
pub fn upgrade_instance(spec: &EffectSpec, g: &mut PropGroup, ids: &mut Ids, layer_size: [f64; 2]) -> bool {
|
||||
let before = g.clone();
|
||||
// Card Wipe's Back Layer was a None / Self popup; it is a layer parameter now, with "Self"
|
||||
// kept in the hidden Back Layer Is Self switch.
|
||||
let mut back_self = None;
|
||||
if spec.id == "ec.transition.cardwipe"
|
||||
&& let Some(pr) = g.get_mut("backLayer")
|
||||
&& matches!(pr.ui, ParamUi::Popup { .. })
|
||||
{
|
||||
back_self = Some(pr.value.as_enum() == 1);
|
||||
pr.value = Value::Layer(None);
|
||||
pr.keys.clear();
|
||||
pr.ui = ParamUi::Layer;
|
||||
}
|
||||
for ps in &spec.params {
|
||||
// Locate the stored property: at its spec path, under an old id, or at top level
|
||||
// before it moved into a twirl-down group.
|
||||
@@ -296,6 +308,11 @@ pub fn upgrade_instance(spec: &EffectSpec, g: &mut PropGroup, ids: &mut Ids, lay
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(on) = back_self
|
||||
&& let Some(pr) = g.get_mut("backSelf")
|
||||
{
|
||||
pr.value = Value::Bool(on);
|
||||
}
|
||||
// Refresh twirl-down group names.
|
||||
fn names(g: &mut PropGroup) {
|
||||
for c in &mut g.children {
|
||||
@@ -368,4 +385,21 @@ mod tests {
|
||||
assert!(g.get("smoothness").is_none());
|
||||
assert_eq!(prop_at(&mut g, "stabilization/smoothness").unwrap().value, Value::Scalar(12.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn card_wipe_back_layer_popup_becomes_a_layer_parameter() {
|
||||
let spec = find("ec.transition.cardwipe").unwrap();
|
||||
let mut next = 1;
|
||||
for (old, self_) in [(1, true), (0, false)] {
|
||||
let mut g = instantiate(spec, &mut Ids(&mut next), "Card Wipe", [100.0, 50.0]);
|
||||
take_prop(&mut g, "backSelf");
|
||||
let pr = g.get_mut("backLayer").unwrap();
|
||||
pr.ui = ParamUi::Popup { options: vec!["None".into(), "Self".into()] };
|
||||
pr.value = Value::Enum(old);
|
||||
assert!(upgrade_instance(spec, &mut g, &mut Ids(&mut next), [100.0, 50.0]));
|
||||
assert_eq!(g.get("backLayer").unwrap().value, Value::Layer(None));
|
||||
assert_eq!(g.get("backLayer").unwrap().ui, ParamUi::Layer);
|
||||
assert_eq!(g.get("backSelf").unwrap().value, Value::Bool(self_));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -431,7 +431,7 @@ mod tests {
|
||||
fn noise_types_differ_and_block_is_piecewise_constant() {
|
||||
let a = run("ec.noise.fractal", &[("noiseType", Value::Enum(0)), ("complexity", num(1.0)), ("transform/scale", num(10.0))]);
|
||||
// Block noise: neighbours inside one 10 px cell share a value.
|
||||
assert_eq!(a.get(1, 1), a.get(2, 2));
|
||||
assert_eq!(a.get(4, 4), a.get(5, 5));
|
||||
let others: Vec<Image> = (1..4).map(|t| run("ec.noise.fractal", &[("noiseType", Value::Enum(t)), ("transform/scale", num(10.0))])).collect();
|
||||
assert_ne!(others[0], others[1]);
|
||||
assert_ne!(others[1], others[2]);
|
||||
|
||||
+416
-65
@@ -10,6 +10,7 @@ use effectcraft_project::ParamUi;
|
||||
use effectcraft_raster::{Image, Px};
|
||||
use rayon::prelude::*;
|
||||
|
||||
use crate::card3d::{Lighting, Proj};
|
||||
use crate::generate::value_noise;
|
||||
use crate::sim::{Acc, Shape, Sprite, h, hs, pct, pt, raster, spec, splat};
|
||||
use crate::util::{Plane, SimCache, gauss_plane, layer_or_self, params_key, unpremul};
|
||||
@@ -17,7 +18,7 @@ use crate::{Buf, EffectCtx, EffectSpec, col, num, p, popup, slider};
|
||||
|
||||
// ------------------------------------------------------------------ 3D pieces
|
||||
|
||||
type M3 = [[f64; 3]; 3];
|
||||
pub(crate) type M3 = [[f64; 3]; 3];
|
||||
|
||||
fn mat_mul(a: &M3, b: &M3) -> M3 {
|
||||
std::array::from_fn(|i| std::array::from_fn(|j| (0..3).map(|k| a[i][k] * b[k][j]).sum()))
|
||||
@@ -35,7 +36,7 @@ fn rot_xyz(x: f64, y: f64, z: f64) -> M3 {
|
||||
}
|
||||
|
||||
/// Rotation by `angle` about unit `axis` (Rodrigues).
|
||||
fn rot_axis(axis: [f64; 3], angle: f64) -> M3 {
|
||||
pub(crate) fn rot_axis(axis: [f64; 3], angle: f64) -> M3 {
|
||||
let l = (axis[0] * axis[0] + axis[1] * axis[1] + axis[2] * axis[2]).sqrt();
|
||||
if l < 1e-12 || angle == 0.0 {
|
||||
return [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
|
||||
@@ -72,9 +73,15 @@ struct Cam {
|
||||
d: f64,
|
||||
}
|
||||
|
||||
impl Cam {
|
||||
fn proj(self) -> Proj {
|
||||
Proj::simple(self.c, self.d)
|
||||
}
|
||||
}
|
||||
|
||||
/// A planar textured piece ready to draw.
|
||||
#[derive(Clone, Copy)]
|
||||
struct Piece {
|
||||
pub(crate) struct Piece {
|
||||
/// Screen (buffer px, homogeneous) → piece-local (a, b, w).
|
||||
inv: M3,
|
||||
/// Rest centre of the piece in texture pixels.
|
||||
@@ -84,34 +91,43 @@ struct Piece {
|
||||
n: u8,
|
||||
bbox: [f32; 4],
|
||||
shade: f32,
|
||||
alpha: f32,
|
||||
/// Lighting: colour multiplier and additive specular highlight.
|
||||
tint: [f32; 3],
|
||||
spec: [f32; 3],
|
||||
pub(crate) alpha: f32,
|
||||
/// Flat straight colour instead of the texture.
|
||||
flat: Option<[f32; 3]>,
|
||||
pub(crate) flat: Option<[f32; 3]>,
|
||||
/// Facing away from the camera (draw the back texture).
|
||||
back: bool,
|
||||
pub(crate) back: bool,
|
||||
/// Back faces sample their texture mirrored about the piece centre: 0 no, 1 vertically
|
||||
/// (flipped about the X axis), 2 horizontally (about the Y axis).
|
||||
pub(crate) back_mirror: u8,
|
||||
depth: f64,
|
||||
}
|
||||
|
||||
impl Piece {
|
||||
/// Place polygon `poly` (texture px, around rest centre `c0`) with rotation `r` and its
|
||||
/// centre at 3D `pos` (buffer px; z away from the camera).
|
||||
fn new(poly: &[[f32; 2]], c0: [f32; 2], r: &M3, pos: [f64; 3], cam: Cam) -> Option<Piece> {
|
||||
/// centre at 3D `pos` (buffer px; z away from the camera), seen through `cam`.
|
||||
pub(crate) fn new(poly: &[[f32; 2]], c0: [f32; 2], r: &M3, pos: [f64; 3], cam: impl Into<Proj>) -> Option<Piece> {
|
||||
let cam: Proj = cam.into();
|
||||
let n = poly.len().min(6);
|
||||
if n < 3 {
|
||||
return None;
|
||||
}
|
||||
let (cx, cy, d) = (cam.c[0], cam.c[1], cam.d);
|
||||
let zc = pos[2];
|
||||
let hm: M3 = [
|
||||
[cx * r[2][0] + d * r[0][0], cx * r[2][1] + d * r[0][1], cx * (d + zc) + d * (pos[0] - cx)],
|
||||
[cy * r[2][0] + d * r[1][0], cy * r[2][1] + d * r[1][1], cy * (d + zc) + d * (pos[1] - cy)],
|
||||
[r[2][0], r[2][1], d + zc],
|
||||
];
|
||||
let m = &cam.m;
|
||||
let row = |i: usize| -> [f64; 3] {
|
||||
[
|
||||
m[i][0] * r[0][0] + m[i][1] * r[1][0] + m[i][2] * r[2][0],
|
||||
m[i][0] * r[0][1] + m[i][1] * r[1][1] + m[i][2] * r[2][1],
|
||||
m[i][0] * pos[0] + m[i][1] * pos[1] + m[i][2] * pos[2] + m[i][3],
|
||||
]
|
||||
};
|
||||
let hm: M3 = [row(0), row(1), row(2)];
|
||||
let mut bb = [f32::INFINITY, f32::INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY];
|
||||
for v in &poly[..n] {
|
||||
let (a, b) = ((v[0] - c0[0]) as f64, (v[1] - c0[1]) as f64);
|
||||
let w = hm[2][0] * a + hm[2][1] * b + hm[2][2];
|
||||
if w < d * 0.05 {
|
||||
if w < cam.near {
|
||||
return None;
|
||||
}
|
||||
let sx = ((hm[0][0] * a + hm[0][1] * b + hm[0][2]) / w) as f32;
|
||||
@@ -126,8 +142,12 @@ impl Piece {
|
||||
if area < 0.0 {
|
||||
pp[..n].reverse();
|
||||
}
|
||||
// Front normal (0, 0, -1) rotated; facing the camera when its z is negative.
|
||||
let nz = -r[2][2];
|
||||
// The front faces -z at rest; it faces the camera when its normal points at the eye.
|
||||
let eye = cam.eye();
|
||||
let nrm = [-r[0][2], -r[1][2], -r[2][2]];
|
||||
let to_eye = [eye[0] - pos[0], eye[1] - pos[1], eye[2] - pos[2]];
|
||||
let facing = nrm[0] * to_eye[0] + nrm[1] * to_eye[1] + nrm[2] * to_eye[2];
|
||||
let depth = hm[2][2];
|
||||
Some(Piece {
|
||||
inv,
|
||||
c0,
|
||||
@@ -135,10 +155,13 @@ impl Piece {
|
||||
n: n as u8,
|
||||
bbox: [bb[0] - 1.0, bb[1] - 1.0, bb[2] + 1.0, bb[3] + 1.0],
|
||||
shade: 1.0,
|
||||
tint: [1.0; 3],
|
||||
spec: [0.0; 3],
|
||||
alpha: 1.0,
|
||||
flat: None,
|
||||
back: nz > 0.0,
|
||||
depth: zc,
|
||||
back: facing < 0.0,
|
||||
back_mirror: 0,
|
||||
depth,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -146,10 +169,23 @@ impl Piece {
|
||||
fn facing(r: &M3) -> f32 {
|
||||
r[2][2].abs() as f32
|
||||
}
|
||||
|
||||
/// Light the piece (rotation `r`, centre `pos`) with Lighting / Material.
|
||||
pub(crate) fn light(&mut self, l: &Lighting, r: &M3, pos: [f64; 3], eye: [f64; 3]) {
|
||||
let (t, s) = l.shade([-r[0][2], -r[1][2], -r[2][2]], pos, eye);
|
||||
self.tint = t;
|
||||
self.spec = s;
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Cam> for Proj {
|
||||
fn from(c: Cam) -> Proj {
|
||||
c.proj()
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw pieces (already sorted far → near) over `out`.
|
||||
fn draw_pieces(out: &mut Image, pieces: &[Piece], front: &Image, back: Option<&Image>) {
|
||||
pub(crate) fn draw_pieces(out: &mut Image, pieces: &[Piece], front: &Image, back: Option<&Image>) {
|
||||
raster(
|
||||
out,
|
||||
pieces,
|
||||
@@ -181,6 +217,11 @@ fn draw_pieces(out: &mut Image, pieces: &[Piece], front: &Image, back: Option<&I
|
||||
Some(c) => (c, 1.0),
|
||||
None => {
|
||||
let tex = if q.back { back.unwrap_or(front) } else { front };
|
||||
let (u, v) = match (q.back, q.back_mirror) {
|
||||
(true, 1) => (u, 2.0 * q.c0[1] - v),
|
||||
(true, 2) => (2.0 * q.c0[0] - u, v),
|
||||
_ => (u, v),
|
||||
};
|
||||
unpremul(tex.sample_bilinear(u as f64, v as f64))
|
||||
}
|
||||
};
|
||||
@@ -189,13 +230,14 @@ fn draw_pieces(out: &mut Image, pieces: &[Piece], front: &Image, back: Option<&I
|
||||
return None;
|
||||
}
|
||||
let k = q.shade;
|
||||
Some([c[0] * k * a, c[1] * k * a, c[2] * k * a, a])
|
||||
let o: [f32; 3] = std::array::from_fn(|i| c[i] * k * q.tint[i] + q.spec[i]);
|
||||
Some([o[0] * a, o[1] * a, o[2] * a, a])
|
||||
},
|
||||
Acc::Over,
|
||||
);
|
||||
}
|
||||
|
||||
fn sort_far_first(pieces: &mut [Piece]) {
|
||||
pub(crate) fn sort_far_first(pieces: &mut [Piece]) {
|
||||
pieces.sort_by(|a, b| b.depth.total_cmp(&a.depth));
|
||||
}
|
||||
|
||||
@@ -430,12 +472,9 @@ fn card_dance(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let [lw, lh] = ctx.layer_size;
|
||||
let s = b.scale;
|
||||
let (cw, ch) = (lw / cols as f64, lh / rows as f64);
|
||||
let zpos = pr.f("cameraPosition/cameraZ").max(0.05);
|
||||
let focal = pr.f("cameraPosition/focalLength").max(1.0);
|
||||
let cam = Cam { c: [b.offset[0] + lw * 0.5 * s, b.offset[1] + lh * 0.5 * s], d: lw.max(lh) * s * zpos * focal / 140.0 };
|
||||
let ambient = pr.f("lighting/ambientLight") as f32;
|
||||
let diffuse = pr.f("material/diffuse") as f32;
|
||||
let intensity = pr.f("lighting/lightIntensity") as f32;
|
||||
let cam = crate::card3d::projection(ctx, &b);
|
||||
let eye = cam.eye();
|
||||
let light = Lighting::from(ctx, &b);
|
||||
let props: Vec<(u32, f64, f64)> =
|
||||
CD_PROPS.iter().map(|(id, g)| (pr.e(&format!("{g}/{id}Source")), pr.f(&format!("{g}/{id}Multiplier")), pr.f(&format!("{g}/{id}Offset")))).collect();
|
||||
let src = b.img.clone();
|
||||
@@ -458,7 +497,7 @@ fn card_dance(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let (tw, th) = ((cw * s) as f32, (ch * s) as f32);
|
||||
let poly = [[tx0, ty0], [tx0 + tw, ty0], [tx0 + tw, ty0 + th], [tx0, ty0 + th]];
|
||||
let mut pc = Piece::new(&poly, [bx as f32, by as f32], &rs, pos, cam)?;
|
||||
pc.shade = ambient + diffuse * intensity * Piece::facing(&r);
|
||||
pc.light(&light, &r, pos, eye);
|
||||
Some(pc)
|
||||
})
|
||||
.collect();
|
||||
@@ -471,7 +510,8 @@ fn card_dance(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
|
||||
// ------------------------------------------------------------------ Shatter
|
||||
|
||||
const SHATTER_PATTERNS: &[&str] = &["Bricks", "Glass", "Hexagons", "Squares", "Triangles"];
|
||||
/// Shatter's Pattern options; the last, Custom, takes its pieces from Custom Shatter Map.
|
||||
const SHATTER_PATTERNS: &[&str] = &["Bricks", "Glass", "Hexagons", "Squares", "Triangles", "Custom"];
|
||||
|
||||
/// Pattern cells (polygons in pattern space, before rotation) covering `[x0, x1]×[y0, y1]`.
|
||||
fn pattern_cells(kind: u32, cell: f64, x0: f64, y0: f64, x1: f64, y1: f64, seed: u32) -> Vec<Vec<[f64; 2]>> {
|
||||
@@ -570,12 +610,40 @@ fn drag_factors(k: f64, t: f64) -> (f64, f64) {
|
||||
((1.0 - e) / k, t / k - (1.0 - e) / (k * k))
|
||||
}
|
||||
}
|
||||
/// Shatter's Front / Side / Back Mode options.
|
||||
const SHATTER_TEXTURE_MODES: [&str; 3] = ["Color", "Layer", "Tinted Layer"];
|
||||
|
||||
/// One face's texture: a flat colour, a layer, or a layer tinted by the Textures colour.
|
||||
struct Face {
|
||||
mode: u32,
|
||||
img: Option<Image>,
|
||||
}
|
||||
|
||||
impl Face {
|
||||
fn from(ctx: &EffectCtx, b: &Buf, mode: &str, layer: &str, default_mode: u32) -> Face {
|
||||
let mode = ctx.params.get(mode).map(|v| v.as_enum()).unwrap_or(default_mode);
|
||||
// A face layer of None is the effect's own layer.
|
||||
let img = (mode != 0).then(|| layer_or_self(ctx, b, layer, true, true));
|
||||
Face { mode, img }
|
||||
}
|
||||
}
|
||||
|
||||
/// Group key of a Custom Shatter Map colour (pieces of one colour break as one).
|
||||
fn map_key(c: [f32; 4]) -> u32 {
|
||||
let (c, a) = unpremul(c);
|
||||
if a < 0.5 {
|
||||
return u32::MAX;
|
||||
}
|
||||
let q = |v: f32| ((v.clamp(0.0, 1.0) * 15.0).round() as u32) & 15;
|
||||
q(c[0]) << 8 | q(c[1]) << 4 | q(c[2])
|
||||
}
|
||||
|
||||
fn shatter(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let pr = ctx.params;
|
||||
let view = pr.e("view");
|
||||
let render = pr.e("render");
|
||||
let kind = pr.e("shape/pattern");
|
||||
let custom = kind as usize == SHATTER_PATTERNS.len() - 1;
|
||||
let reps = pr.f("shape/repetitions").clamp(1.0, 500.0);
|
||||
let dir = pr.f("shape/direction").to_radians();
|
||||
let origin = pr.v2("shape/origin");
|
||||
@@ -591,15 +659,29 @@ fn shatter(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let gravity = pr.f("physics/gravity");
|
||||
let gdir = pr.f("physics/gravityDirection").to_radians();
|
||||
let ginc = pr.f("physics/gravityInclination").to_radians();
|
||||
let ambient = pr.f("lighting/ambientLight") as f32;
|
||||
let intensity = pr.f("lighting/lightIntensity") as f32;
|
||||
let seed = (pr.f("randomSeed") as u32).wrapping_mul(0x9e3779b1) ^ ctx.seed;
|
||||
let [lw, lh] = ctx.layer_size;
|
||||
let s = b.scale;
|
||||
let t = ctx.time.max(0.0);
|
||||
let unit = lw * 0.1;
|
||||
let cell = lw / reps;
|
||||
let cam = Cam { c: [b.offset[0] + lw * 0.5 * s, b.offset[1] + lh * 0.5 * s], d: 2.0 * lw.max(lh) * s };
|
||||
let extrude = pr.f("shape/extrusionDepth").max(0.0) * cell * s;
|
||||
let cam = crate::card3d::projection(ctx, &b);
|
||||
let eye = cam.eye();
|
||||
let light = Lighting::from(ctx, &b);
|
||||
// Gradient: with a gradient layer only pieces at least as bright as 1 − Shatter Threshold
|
||||
// (Invert Gradient: as dark) break.
|
||||
let gradient = ctx.layer_param("gradient/gradientLayer", true).map(|o| crate::util::fit_layer(ctx, &b, &o, true));
|
||||
let threshold = pr.get("gradient/shatterThreshold").map(Value::as_f64).unwrap_or(0.0) / 100.0;
|
||||
let invert_gradient = pr.b("gradient/invertGradient");
|
||||
// Textures.
|
||||
let tex_color = pr.get("textures/color").map(|v| v.as_color()).unwrap_or([1.0; 4]);
|
||||
let tex_opacity = pr.get("textures/opacity").map(Value::as_f64).unwrap_or(1.0).clamp(0.0, 1.0) as f32;
|
||||
let front = Face::from(ctx, &b, "textures/frontMode", "textures/frontLayer", 1);
|
||||
let side = Face::from(ctx, &b, "textures/sideMode", "textures/sideLayer", 0);
|
||||
let backf = Face::from(ctx, &b, "textures/backMode", "textures/backLayer", 1);
|
||||
let custom_map = custom.then(|| layer_or_self(ctx, &b, "shape/customShatterMap", true, true));
|
||||
let white_fixed = pr.b("shape/whiteTilesFixed");
|
||||
let (sd, cd) = dir.sin_cos();
|
||||
let to_layer = |q: [f64; 2]| [origin[0] + q[0] * cd - q[1] * sd, origin[1] + q[0] * sd + q[1] * cd];
|
||||
// Pattern-space bounds of the layer rectangle.
|
||||
@@ -609,21 +691,45 @@ fn shatter(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
});
|
||||
let (px0, px1) = (corners.iter().map(|c| c[0]).fold(f64::INFINITY, f64::min), corners.iter().map(|c| c[0]).fold(f64::NEG_INFINITY, f64::max));
|
||||
let (py0, py1) = (corners.iter().map(|c| c[1]).fold(f64::INFINITY, f64::min), corners.iter().map(|c| c[1]).fold(f64::NEG_INFINITY, f64::max));
|
||||
let cells = pattern_cells(kind, cell, px0, py0, px1, py1, seed);
|
||||
// Custom Shatter Map: square cells grouped by the map's colour.
|
||||
let cells = pattern_cells(if custom { 3 } else { kind }, cell, px0, py0, px1, py1, seed);
|
||||
let polys: Vec<Vec<[f64; 2]>> = cells.iter().map(|poly| poly.iter().map(|&q| to_layer(q)).collect()).collect();
|
||||
let centre = |lp: &[[f64; 2]]| [lp.iter().map(|v| v[0]).sum::<f64>() / lp.len() as f64, lp.iter().map(|v| v[1]).sum::<f64>() / lp.len() as f64];
|
||||
let keys: Vec<u32> = match &custom_map {
|
||||
Some(m) => polys
|
||||
.iter()
|
||||
.map(|lp| {
|
||||
let c = centre(lp);
|
||||
map_key(m.sample_bilinear(c[0] * s + b.offset[0], c[1] * s + b.offset[1]))
|
||||
})
|
||||
.collect(),
|
||||
None => (0..polys.len() as u32).collect(),
|
||||
};
|
||||
// Group centres (custom map) — each group moves as one rigid piece.
|
||||
let mut groups: std::collections::HashMap<u32, ([f64; 2], f64)> = std::collections::HashMap::new();
|
||||
for (lp, &key) in polys.iter().zip(&keys) {
|
||||
let c = centre(lp);
|
||||
let e = groups.entry(key).or_insert(([0.0; 2], 0.0));
|
||||
e.0[0] += c[0];
|
||||
e.0[1] += c[1];
|
||||
e.1 += 1.0;
|
||||
}
|
||||
let g3 = [gdir.sin() * ginc.cos() * gravity * unit, -gdir.cos() * ginc.cos() * gravity * unit, ginc.sin() * gravity * unit];
|
||||
let src = b.img.clone();
|
||||
let mut pieces: Vec<Piece> = cells
|
||||
let mut pieces: Vec<Piece> = polys
|
||||
.par_iter()
|
||||
.enumerate()
|
||||
.filter_map(|(i, poly)| {
|
||||
let i = i as u32;
|
||||
let lp: Vec<[f64; 2]> = poly.iter().map(|&q| to_layer(q)).collect();
|
||||
.zip(keys.par_iter())
|
||||
.flat_map_iter(|(lp, &key)| {
|
||||
let mut out: Vec<Piece> = vec![];
|
||||
let (mnx, mxx) = (lp.iter().map(|v| v[0]).fold(f64::INFINITY, f64::min), lp.iter().map(|v| v[0]).fold(f64::NEG_INFINITY, f64::max));
|
||||
let (mny, mxy) = (lp.iter().map(|v| v[1]).fold(f64::INFINITY, f64::min), lp.iter().map(|v| v[1]).fold(f64::NEG_INFINITY, f64::max));
|
||||
if mxx < 0.0 || mxy < 0.0 || mnx > lw || mny > lh {
|
||||
return None;
|
||||
if mxx < 0.0 || mxy < 0.0 || mnx > lw || mny > lh || key == u32::MAX {
|
||||
return out.into_iter();
|
||||
}
|
||||
let c = [lp.iter().map(|v| v[0]).sum::<f64>() / lp.len() as f64, lp.iter().map(|v| v[1]).sum::<f64>() / lp.len() as f64];
|
||||
let pc_c = centre(lp);
|
||||
let (gsum, gn) = groups[&key];
|
||||
let gc = [gsum[0] / gn, gsum[1] / gn];
|
||||
let i = key;
|
||||
// Broken when inside a force sphere (centred `depth` in front of the layer).
|
||||
let mut v = [0.0f64; 3];
|
||||
let mut broken = false;
|
||||
@@ -632,7 +738,7 @@ fn shatter(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
if rad <= 0.0 {
|
||||
continue;
|
||||
}
|
||||
let dv = [c[0] - fp[0], c[1] - fp[1], fd * lw];
|
||||
let dv = [gc[0] - fp[0], gc[1] - fp[1], fd * lw];
|
||||
let dist = (dv[0] * dv[0] + dv[1] * dv[1] + dv[2] * dv[2]).sqrt();
|
||||
if dist < rad {
|
||||
broken = true;
|
||||
@@ -642,19 +748,31 @@ fn shatter(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (broken && render == 1) || (!broken && render == 2) {
|
||||
return None;
|
||||
if broken && let Some(g) = &gradient {
|
||||
let (c, _) = unpremul(g.sample_bilinear(gc[0] * s + b.offset[0], gc[1] * s + b.offset[1]));
|
||||
let mut l = effectcraft_color::luminance(c[0], c[1], c[2]).clamp(0.0, 1.0) as f64;
|
||||
if invert_gradient {
|
||||
l = 1.0 - l;
|
||||
}
|
||||
broken = threshold > 0.0 && l >= 1.0 - threshold;
|
||||
}
|
||||
let mut pos = [c[0], c[1], 0.0];
|
||||
if broken && custom && white_fixed && key == 0xfff {
|
||||
broken = false;
|
||||
}
|
||||
if (broken && render == 1) || (!broken && render == 2) {
|
||||
return out.into_iter();
|
||||
}
|
||||
let mut gpos = [gc[0], gc[1], 0.0];
|
||||
let mut r = rot_axis([1.0, 0.0, 0.0], 0.0);
|
||||
if broken && t > 0.0 {
|
||||
let moving = broken && t > 0.0;
|
||||
if moving {
|
||||
let mass = 1.0 + mass_var * hs(i, 1, seed) as f64;
|
||||
for q in 0..3 {
|
||||
v[q] = v[q] / mass + randomness * unit * hs(i, 2 + q as u32, seed) as f64 * 2.0;
|
||||
}
|
||||
let (f1, f2) = drag_factors(k, t);
|
||||
for q in 0..3 {
|
||||
pos[q] += v[q] * f1 + g3[q] * f2;
|
||||
gpos[q] += v[q] * f1 + g3[q] * f2;
|
||||
}
|
||||
let axis = match tumble {
|
||||
1 => [0.0, 0.0, 0.0],
|
||||
@@ -670,18 +788,66 @@ fn shatter(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let omega = rot_speed * std::f64::consts::TAU * (0.5 + h(i, 9, seed) as f64) * speed.min(10.0);
|
||||
r = rot_axis(axis, omega * f1);
|
||||
}
|
||||
// The piece's centre: its offset from the group centre, rotated with the group.
|
||||
let off = [pc_c[0] - gc[0], pc_c[1] - gc[1], 0.0];
|
||||
let pos: [f64; 3] = std::array::from_fn(|q| gpos[q] + (0..3).map(|k| r[q][k] * off[k]).sum::<f64>());
|
||||
let tex: Vec<[f32; 2]> = lp.iter().map(|v| [(v[0] * s + b.offset[0]) as f32, (v[1] * s + b.offset[1]) as f32]).collect();
|
||||
let c0 = [(c[0] * s + b.offset[0]) as f32, (c[1] * s + b.offset[1]) as f32];
|
||||
let c0 = [(pc_c[0] * s + b.offset[0]) as f32, (pc_c[1] * s + b.offset[1]) as f32];
|
||||
let posb = [pos[0] * s + b.offset[0], pos[1] * s + b.offset[1], pos[2] * s];
|
||||
let mut pc = Piece::new(&tex, c0, &r, posb, cam)?;
|
||||
pc.shade = ambient + (1.0 - ambient) * intensity * Piece::facing(&r);
|
||||
Some(pc)
|
||||
let Some(mut pc) = Piece::new(&tex, c0, &r, posb, cam) else { return out.into_iter() };
|
||||
pc.light(&light, &r, posb, eye);
|
||||
pc.alpha = tex_opacity;
|
||||
let face = if pc.back { &backf } else { &front };
|
||||
if face.mode == 0 {
|
||||
pc.flat = Some([tex_color[0], tex_color[1], tex_color[2]]);
|
||||
} else if face.mode == 2 {
|
||||
pc.tint = std::array::from_fn(|q| pc.tint[q] * tex_color[q]);
|
||||
}
|
||||
// Extrusion: the sides of a moving piece, from its front outline back by the depth.
|
||||
if moving && extrude > 0.0 {
|
||||
let nb = [r[0][2], r[1][2], r[2][2]];
|
||||
let n = tex.len();
|
||||
for e in 0..n {
|
||||
let (a, bv) = (tex[e], tex[(e + 1) % n]);
|
||||
let ed = [(bv[0] - a[0]) as f64, (bv[1] - a[1]) as f64];
|
||||
let len = (ed[0] * ed[0] + ed[1] * ed[1]).sqrt();
|
||||
if len < 1e-6 {
|
||||
continue;
|
||||
}
|
||||
let ew: [f64; 3] = std::array::from_fn(|q| (r[q][0] * ed[0] + r[q][1] * ed[1]) / len);
|
||||
let third = [ew[1] * nb[2] - ew[2] * nb[1], ew[2] * nb[0] - ew[0] * nb[2], ew[0] * nb[1] - ew[1] * nb[0]];
|
||||
let rs: M3 = std::array::from_fn(|q| [ew[q], nb[q], third[q]]);
|
||||
// Side centre in world: edge midpoint (world) plus half the depth backwards.
|
||||
let mid = [((a[0] + bv[0]) * 0.5 - c0[0]) as f64, ((a[1] + bv[1]) * 0.5 - c0[1]) as f64];
|
||||
let mw: [f64; 3] = std::array::from_fn(|q| posb[q] + r[q][0] * mid[0] + r[q][1] * mid[1] + nb[q] * extrude * 0.5);
|
||||
let (hl, hd) = ((len * 0.5) as f32, (extrude * 0.5) as f32);
|
||||
let cs = [(a[0] + bv[0]) * 0.5, (a[1] + bv[1]) * 0.5];
|
||||
let quad = [[cs[0] - hl, cs[1] - hd], [cs[0] + hl, cs[1] - hd], [cs[0] + hl, cs[1] + hd], [cs[0] - hl, cs[1] + hd]];
|
||||
if let Some(mut sp) = Piece::new(&quad, cs, &rs, mw, cam) {
|
||||
sp.light(&light, &rs, mw, eye);
|
||||
sp.alpha = tex_opacity;
|
||||
let colour = match (&side.img, side.mode) {
|
||||
(Some(img), m) if m != 0 => {
|
||||
let (c, _) = unpremul(img.sample_bilinear(cs[0] as f64, cs[1] as f64));
|
||||
if m == 2 { std::array::from_fn(|q| c[q] * tex_color[q]) } else { c }
|
||||
}
|
||||
_ => [tex_color[0], tex_color[1], tex_color[2]],
|
||||
};
|
||||
sp.flat = Some(colour);
|
||||
out.push(sp);
|
||||
}
|
||||
}
|
||||
}
|
||||
out.push(pc);
|
||||
out.into_iter()
|
||||
})
|
||||
.collect();
|
||||
sort_far_first(&mut pieces);
|
||||
let mut out = Image::new(src.width, src.height);
|
||||
let front_img = front.img.as_ref().unwrap_or(&src);
|
||||
let back_img = backf.img.as_ref().unwrap_or(front_img);
|
||||
if view == 0 {
|
||||
draw_pieces(&mut out, &pieces, &src, None);
|
||||
draw_pieces(&mut out, &pieces, front_img, Some(back_img));
|
||||
} else {
|
||||
// Wireframes: piece outlines (front view: at rest; otherwise animated), plus forces.
|
||||
let animated = view == 2 || view == 4;
|
||||
@@ -1277,13 +1443,9 @@ fn card_dance_params() -> Vec<crate::ParamSpec> {
|
||||
v.push(p(n[1], "Multiplier", num(0.0), slider(lo, hi, if rot { -360.0 } else { -10.0 }, if rot { 360.0 } else { 10.0 }, 2)));
|
||||
v.push(p(n[2], "Offset", num(if scale { 1.0 } else { 0.0 }), slider(lo, hi, if rot { -360.0 } else { -10.0 }, if rot { 360.0 } else { 10.0 }, 2)));
|
||||
}
|
||||
v.extend([
|
||||
p("cameraPosition/cameraZ", "Z Position", num(2.0), slider(0.05, 10.0, 0.5, 5.0, 2)),
|
||||
p("cameraPosition/focalLength", "Focal Length", num(70.0), slider(1.0, 500.0, 10.0, 200.0, 1)),
|
||||
p("lighting/lightIntensity", "Light Intensity", num(1.0), slider(0.0, 4.0, 0.0, 2.0, 2)),
|
||||
p("lighting/ambientLight", "Ambient Light", num(0.25), slider(0.0, 2.0, 0.0, 1.0, 2)),
|
||||
p("material/diffuse", "Diffuse Reflection", num(0.75), slider(0.0, 2.0, 0.0, 1.0, 2)),
|
||||
]);
|
||||
v.extend(crate::card3d::camera_params(2.0));
|
||||
v.extend(crate::card3d::lighting_params(1.0, 0.25));
|
||||
v.extend(crate::card3d::material_params(0.75));
|
||||
v
|
||||
}
|
||||
|
||||
@@ -1399,6 +1561,8 @@ pub fn specs() -> Vec<EffectSpec> {
|
||||
p("shape/repetitions", "Repetitions", num(10.0), slider(1.0, 500.0, 1.0, 100.0, 2)),
|
||||
p("shape/direction", "Direction", num(0.0), ParamUi::Angle),
|
||||
p("shape/origin", "Origin", pt(0.5, 0.5), ParamUi::Point),
|
||||
p("shape/customShatterMap", "Custom Shatter Map", Value::Layer(None), ParamUi::Layer),
|
||||
p("shape/whiteTilesFixed", "White Tiles Fixed", Value::Bool(false), ParamUi::Checkbox),
|
||||
p("shape/extrusionDepth", "Extrusion Depth", num(0.05), slider(0.0, 1.0, 0.0, 1.0, 3)),
|
||||
p("force1/force1Position", "Position", pt(0.5, 0.5), ParamUi::Point),
|
||||
p("force1/force1Depth", "Depth", num(0.1), slider(-1.0, 1.0, -1.0, 1.0, 3)),
|
||||
@@ -1416,10 +1580,24 @@ pub fn specs() -> Vec<EffectSpec> {
|
||||
p("physics/gravity", "Gravity", num(3.0), slider(0.0, 100.0, 0.0, 10.0, 2)),
|
||||
p("physics/gravityDirection", "Gravity Direction", num(180.0), ParamUi::Angle),
|
||||
p("physics/gravityInclination", "Gravity Inclination", num(0.0), slider(-90.0, 90.0, -90.0, 90.0, 1)),
|
||||
p("lighting/lightIntensity", "Light Intensity", num(1.0), slider(0.0, 4.0, 0.0, 2.0, 2)),
|
||||
p("lighting/ambientLight", "Ambient Light", num(0.25), slider(0.0, 2.0, 0.0, 1.0, 2)),
|
||||
p("randomSeed", "Random Seed", num(0.0), slider(0.0, 10_000.0, 0.0, 1000.0, 0)),
|
||||
],
|
||||
p("gradient/shatterThreshold", "Shatter Threshold", num(0.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
p("gradient/gradientLayer", "Gradient Layer", Value::Layer(None), ParamUi::Layer),
|
||||
p("gradient/invertGradient", "Invert Gradient", Value::Bool(false), ParamUi::Checkbox),
|
||||
p("textures/color", "Color", col(1.0, 1.0, 1.0), ParamUi::Color),
|
||||
p("textures/opacity", "Opacity", num(1.0), slider(0.0, 1.0, 0.0, 1.0, 3)),
|
||||
p("textures/frontMode", "Front Mode", Value::Enum(1), popup(&SHATTER_TEXTURE_MODES)),
|
||||
p("textures/sideMode", "Side Mode", Value::Enum(0), popup(&SHATTER_TEXTURE_MODES)),
|
||||
p("textures/backMode", "Back Mode", Value::Enum(1), popup(&SHATTER_TEXTURE_MODES)),
|
||||
p("textures/frontLayer", "Front Layer", Value::Layer(None), ParamUi::Layer),
|
||||
p("textures/sideLayer", "Side Layer", Value::Layer(None), ParamUi::Layer),
|
||||
p("textures/backLayer", "Back Layer", Value::Layer(None), ParamUi::Layer),
|
||||
]
|
||||
.into_iter()
|
||||
.chain(crate::card3d::camera_params(4.0))
|
||||
.chain(crate::card3d::lighting_params(1.0, 0.25))
|
||||
.chain(crate::card3d::material_params(0.75))
|
||||
.chain([p("randomSeed", "Random Seed", num(0.0), slider(0.0, 10_000.0, 0.0, 1000.0, 0))])
|
||||
.collect(),
|
||||
shatter,
|
||||
),
|
||||
spec(
|
||||
@@ -1651,4 +1829,177 @@ mod tests {
|
||||
};
|
||||
assert!(count(0.0) < count(20.0) * 0.9, "{} vs {}", count(0.0), count(20.0));
|
||||
}
|
||||
|
||||
/// A host with a comp camera (identity, or tilted) and a first comp light.
|
||||
struct Scene {
|
||||
camera: Option<[[f64; 4]; 3]>,
|
||||
light: Option<crate::CompLight>,
|
||||
layer: Option<Image>,
|
||||
}
|
||||
impl crate::EffectHost for Scene {
|
||||
fn layer(&self, _: u64, _: bool) -> Option<crate::LayerPixels> {
|
||||
let img = self.layer.clone()?;
|
||||
let size = [img.width as f64, img.height as f64];
|
||||
Some(crate::LayerPixels { buf: Buf { img, offset: [0.0; 2], scale: 1.0 }, size })
|
||||
}
|
||||
fn audio(&self, _: u64, _: f64, _: usize, _: u32) -> Option<Vec<f32>> {
|
||||
None
|
||||
}
|
||||
fn comp_scene(&self) -> Option<crate::CompScene> {
|
||||
Some(crate::CompScene { camera: self.camera, light: self.light })
|
||||
}
|
||||
}
|
||||
|
||||
fn run_env(id: &str, vals: &[(&str, Value)], img: Image, t: f64, host: &Scene) -> Image {
|
||||
run_fx(id, vals, img, t, EffectEnv { host: Some(host), ..Default::default() }).img
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn card_dance_camera_systems() {
|
||||
let src = ramp(40, 30);
|
||||
// Camera Position: rotating the camera changes the view; zero rotation is the layer.
|
||||
let r = run("ec.sim.carddance", &[("cameraPosition/yRotation", num(30.0))], src.clone(), 0.0);
|
||||
assert!(sum_diff(&r, &src) > 10.0);
|
||||
let order = run("ec.sim.carddance", &[("cameraPosition/yRotation", num(30.0)), ("cameraPosition/xRotation", num(30.0))], src.clone(), 0.0);
|
||||
let order2 = run(
|
||||
"ec.sim.carddance",
|
||||
&[("cameraPosition/yRotation", num(30.0)), ("cameraPosition/xRotation", num(30.0)), ("cameraPosition/transformOrder", Value::Enum(5))],
|
||||
src.clone(),
|
||||
0.0,
|
||||
);
|
||||
assert!(sum_diff(&order, &order2) > 1.0, "Transform Order matters");
|
||||
// X, Y Position moves the view: the layer shifts by the offset.
|
||||
let moved = run("ec.sim.carddance", &[("cameraPosition/xyPosition", pt(30.0, 15.0))], src.clone(), 0.0);
|
||||
assert!((moved.get(25, 10)[0] - src.get(15, 10)[0]).abs() < 0.03, "{:?} {:?}", moved.get(25, 10), src.get(15, 10));
|
||||
// Corner Pins: the layer's corners land on the pins.
|
||||
let pins = [
|
||||
("cameraSystem", Value::Enum(1)),
|
||||
("cornerPins/upperLeftCorner", pt(10.0, 5.0)),
|
||||
("cornerPins/upperRightCorner", pt(30.0, 5.0)),
|
||||
("cornerPins/lowerLeftCorner", pt(10.0, 25.0)),
|
||||
("cornerPins/lowerRightCorner", pt(30.0, 25.0)),
|
||||
];
|
||||
let cp = run("ec.sim.carddance", &pins, src.clone(), 0.0);
|
||||
assert_eq!(cp.get(3, 3)[3], 0.0, "outside the pinned quad");
|
||||
assert!(cp.get(20, 15)[3] > 0.99);
|
||||
assert!((cp.get(20, 15)[0] - src.get(20, 15)[0]).abs() < 0.05, "centre maps to centre");
|
||||
// Comp Camera: the identity camera reproduces the layer; a camera that doubles x moves it.
|
||||
let ident = Scene { camera: Some([[1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 0.002, 1.0]]), light: None, layer: None };
|
||||
let cc = run_env("ec.sim.carddance", &[("cameraSystem", Value::Enum(2))], src.clone(), 0.0, &ident);
|
||||
assert!(max_diff(&cc, &src) < 0.02, "{}", max_diff(&cc, &src));
|
||||
let half = Scene { camera: Some([[0.5, 0.0, 0.0, 0.0], [0.0, 0.5, 0.0, 0.0], [0.0, 0.0, 0.002, 1.0]]), ..ident };
|
||||
let cc = run_env("ec.sim.carddance", &[("cameraSystem", Value::Enum(2))], src.clone(), 0.0, &half);
|
||||
assert_eq!(cc.get(35, 25)[3], 0.0, "half-size view leaves the far corner empty");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn card_dance_lighting_and_material() {
|
||||
let src = Image::filled(40, 30, [0.5, 0.5, 0.5, 1.0]);
|
||||
let tilt = [("rows", num(1.0)), ("columns", num(1.0)), ("yRotation/yRotSource", Value::Enum(0)), ("yRotation/yRotOffset", num(60.0))];
|
||||
let lit = |extra: &[(&str, Value)]| {
|
||||
let mut v: Vec<(&str, Value)> = tilt.to_vec();
|
||||
v.extend_from_slice(extra);
|
||||
run("ec.sim.carddance", &v, src.clone(), 0.0).get(20, 15)
|
||||
};
|
||||
let base = lit(&[]);
|
||||
// Tilted cards facing away from a frontal light get darker than flat ones.
|
||||
assert!(base[0] < 0.45, "{base:?}");
|
||||
// Light colour tints; intensity brightens; ambient alone lights evenly.
|
||||
let red = lit(&[("lighting/lightColor", col(1.0, 0.0, 0.0))]);
|
||||
assert!(red[0] > red[1] + 0.05);
|
||||
assert!(lit(&[("lighting/lightIntensity", num(2.0))])[0] > base[0]);
|
||||
let amb = lit(&[("lighting/ambientLight", num(1.0)), ("material/diffuse", num(0.0))]);
|
||||
assert!((amb[0] - 0.5).abs() < 1e-3);
|
||||
// A light placed to the side (Point Source) changes the shading of the tilted card.
|
||||
let side = lit(&[("lighting/lightType", Value::Enum(1)), ("lighting/lightPosition", pt(200.0, 15.0)), ("lighting/lightDepth", num(0.2))]);
|
||||
assert!((side[0] - base[0]).abs() > 0.02, "{side:?} vs {base:?}");
|
||||
// Specular adds a highlight on flat, frontally lit cards.
|
||||
let flat = run("ec.sim.carddance", &[("material/specular", num(0.5))], src.clone(), 0.0).get(20, 15);
|
||||
assert!(flat[0] > 0.9, "{flat:?}");
|
||||
// First Comp Light: no light in the comp → ambient only; a light from the host is used.
|
||||
let none = Scene { camera: None, light: None, layer: None };
|
||||
let a = run_env("ec.sim.carddance", &[("lighting/lightType", Value::Enum(2))], src.clone(), 0.0, &none).get(20, 15);
|
||||
assert!((a[0] - 0.5 * (0.25 + 0.75)).abs() < 1e-3, "{a:?}");
|
||||
let blue = Scene { light: Some(crate::CompLight { pos: [20.0, 15.0, -100.0], dir: [0.0, 0.0, 1.0], color: [0.0, 0.0, 1.0], kind: 0 }), ..none };
|
||||
let a = run_env("ec.sim.carddance", &[("lighting/lightType", Value::Enum(2))], src.clone(), 0.0, &blue).get(20, 15);
|
||||
assert!(a[2] > a[0] + 0.2, "{a:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shatter_custom_map_gradient_textures_and_extrusion() {
|
||||
let src = ramp(32, 32);
|
||||
let base = [("view", Value::Enum(0)), ("force1/force1Position", pt(16.0, 16.0)), ("force1/force1Radius", num(2.0)), ("shape/origin", pt(16.0, 16.0))];
|
||||
let with = |extra: &[(&str, Value)], t: f64, host: Option<&Scene>| {
|
||||
let mut v: Vec<(&str, Value)> = base.to_vec();
|
||||
v.extend_from_slice(extra);
|
||||
match host {
|
||||
Some(h) => run_env("ec.sim.shatter", &v, src.clone(), t, h),
|
||||
None => run("ec.sim.shatter", &v, src.clone(), t),
|
||||
}
|
||||
};
|
||||
// Custom Shatter Map: a map white on the left and black on the right; with White Tiles
|
||||
// Fixed the left half stays put while the right half breaks away.
|
||||
let mut map = Image::new(32, 32);
|
||||
for y in 0..32 {
|
||||
for x in 0..32 {
|
||||
map.set(x, y, if x < 16 { [1.0; 4] } else { [0.0, 0.0, 0.0, 1.0] });
|
||||
}
|
||||
}
|
||||
let host = Scene { camera: None, light: None, layer: Some(map) };
|
||||
let custom = [("shape/pattern", Value::Enum(5)), ("shape/customShatterMap", Value::Layer(Some(1))), ("shape/whiteTilesFixed", Value::Bool(true))];
|
||||
let at_rest = with(&custom, 0.0, Some(&host));
|
||||
assert!(max_diff(&at_rest, &src) < 0.05, "custom pieces tile the layer: {}", max_diff(&at_rest, &src));
|
||||
let broke = with(&custom, 1.0, Some(&host));
|
||||
assert!(max_diff_region(&broke, &src, 0..12) < 0.05, "white tiles fixed");
|
||||
assert!(sum_diff_region(&broke, &src, 20..32) > 5.0, "black group moved");
|
||||
// Gradient: a black gradient layer with threshold 50 % keeps everything together.
|
||||
let black = Scene { camera: None, light: None, layer: Some(Image::filled(32, 32, [0.0, 0.0, 0.0, 1.0])) };
|
||||
let g = with(&[("gradient/gradientLayer", Value::Layer(Some(1))), ("gradient/shatterThreshold", num(50.0))], 1.0, Some(&black));
|
||||
assert!(max_diff(&g, &src) < 0.05);
|
||||
let gi = with(
|
||||
&[("gradient/gradientLayer", Value::Layer(Some(1))), ("gradient/shatterThreshold", num(50.0)), ("gradient/invertGradient", Value::Bool(true))],
|
||||
1.0,
|
||||
Some(&black),
|
||||
);
|
||||
assert!(sum_diff(&gi, &src) > 10.0);
|
||||
// Textures: Front Mode Color paints the pieces flat; Opacity fades them.
|
||||
let flat = with(&[("textures/frontMode", Value::Enum(0)), ("textures/color", col(0.0, 1.0, 0.0))], 0.0, None);
|
||||
let p = flat.get(16, 16);
|
||||
assert!(p[1] > 0.95 && p[0] < 0.05, "{p:?}");
|
||||
let half = with(&[("textures/opacity", num(0.5)), ("shape/repetitions", num(2.0))], 0.0, None);
|
||||
assert!((half.get(8, 4)[3] - 0.5).abs() < 0.02, "{:?}", half.get(8, 4));
|
||||
// Extrusion: thicker pieces cover more once they tumble.
|
||||
let cover = |d: f64| {
|
||||
with(&[("shape/extrusionDepth", num(d)), ("textures/sideMode", Value::Enum(0)), ("textures/color", col(1.0, 1.0, 1.0))], 0.6, None)
|
||||
.data
|
||||
.iter()
|
||||
.map(|p| p[3])
|
||||
.sum::<f32>()
|
||||
};
|
||||
assert!(cover(1.0) > cover(0.0) + 1.0, "{} vs {}", cover(1.0), cover(0.0));
|
||||
// Deterministic and seek-consistent.
|
||||
assert_eq!(with(&custom, 0.7, Some(&host)), with(&custom, 0.7, Some(&host)));
|
||||
}
|
||||
|
||||
fn max_diff_region(a: &Image, b: &Image, xs: std::ops::Range<u32>) -> f32 {
|
||||
let mut m = 0.0f32;
|
||||
for y in 0..a.height {
|
||||
for x in xs.clone() {
|
||||
let (p, q) = (a.get(x as i64, y as i64), b.get(x as i64, y as i64));
|
||||
m = (0..4).map(|k| (p[k] - q[k]).abs()).fold(m, f32::max);
|
||||
}
|
||||
}
|
||||
m
|
||||
}
|
||||
|
||||
fn sum_diff_region(a: &Image, b: &Image, xs: std::ops::Range<u32>) -> f32 {
|
||||
let mut s = 0.0;
|
||||
for y in 0..a.height {
|
||||
for x in xs.clone() {
|
||||
let (p, q) = (a.get(x as i64, y as i64), b.get(x as i64, y as i64));
|
||||
s += (0..4).map(|k| (p[k] - q[k]).abs()).sum::<f32>();
|
||||
}
|
||||
}
|
||||
s
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
//! Transition effects (batch 2): Block Dissolve, Gradient Wipe (gradient layer or self), Iris Wipe, Card
|
||||
//! Wipe (2D cards), CC Grid Wipe, CC Radial ScaleWipe, CC Scale Wipe and CC Light Wipe.
|
||||
//! Wipe (3D cards with camera, lighting and jitter), CC Grid Wipe, CC Radial ScaleWipe, CC Scale Wipe and CC Light Wipe.
|
||||
//!
|
||||
//! Every transition is the identity at 0 % completion. Angles are clockwise from "up".
|
||||
|
||||
@@ -9,6 +9,7 @@ use effectcraft_project::ParamUi;
|
||||
use effectcraft_raster::Px;
|
||||
use rayon::prelude::*;
|
||||
|
||||
use crate::sim2::{Piece, draw_pieces, rot_axis, sort_far_first};
|
||||
use crate::util::{Plane, gauss_plane, hash1, layer_rect, remap, unpremul};
|
||||
use crate::{Buf, EffectCtx, EffectSpec, ParamSpec, col, num, p, popup, slider};
|
||||
|
||||
@@ -163,21 +164,47 @@ fn iris_wipe(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
b
|
||||
}
|
||||
|
||||
/// Card Wipe: the layer is cut into Rows × Columns cards that flip over in Flip Order, each
|
||||
/// through a 180° turn about Flip Axis, as textured planes in 3D seen through the Camera
|
||||
/// System and lit by Lighting / Material. The flipped side shows Back Layer (none: the cards
|
||||
/// vanish as they turn away). Position / Rotation Jitter wobble the cards over time.
|
||||
fn card_wipe(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let done = completion(ctx);
|
||||
if done <= 0.0 {
|
||||
return b;
|
||||
let pr = ctx.params;
|
||||
let f = |id: &str| pr.get(id).map(Value::as_f64).unwrap_or(0.0);
|
||||
let jitter_on = [
|
||||
"positionJitter/xJitterAmount",
|
||||
"positionJitter/yJitterAmount",
|
||||
"positionJitter/zJitterAmount",
|
||||
"rotationJitter/xRotJitterAmount",
|
||||
"rotationJitter/yRotJitterAmount",
|
||||
"rotationJitter/zRotJitterAmount",
|
||||
]
|
||||
.iter()
|
||||
.any(|id| f(id) != 0.0);
|
||||
if done <= 0.0
|
||||
&& !jitter_on
|
||||
&& pr.e("cameraSystem") == 0
|
||||
&& f("cameraPosition/xRotation") == 0.0
|
||||
&& f("cameraPosition/yRotation") == 0.0
|
||||
&& f("cameraPosition/zRotation") == 0.0
|
||||
{
|
||||
// Nothing has moved: the layer as is (exactly).
|
||||
let xy = pr.get("cameraPosition/xyPosition").map(|v| v.as_vec2());
|
||||
if xy.is_none_or(|p| (p[0] - ctx.layer_size[0] * 0.5).abs() < 1e-9 && (p[1] - ctx.layer_size[1] * 0.5).abs() < 1e-9) {
|
||||
return b;
|
||||
}
|
||||
}
|
||||
let tw = (ctx.params.f("transitionWidth") / 100.0).clamp(0.01, 1.0);
|
||||
let rows = ctx.params.f("rows").round().clamp(1.0, 1000.0) as i64;
|
||||
let tw = (pr.f("transitionWidth") / 100.0).clamp(0.01, 1.0);
|
||||
let rows = pr.f("rows").round().clamp(1.0, 1000.0) as i64;
|
||||
// Rows & Columns: Independent, or Columns Follows Rows.
|
||||
let cols = if ctx.params.e("rowsAndColumns") == 1 { rows } else { ctx.params.f("columns").round().clamp(1.0, 1000.0) as i64 };
|
||||
let card_scale = ctx.params.f("cardScale").max(0.01);
|
||||
let axis = ctx.params.e("flipAxis");
|
||||
let order = ctx.params.e("flipOrder");
|
||||
let back_self = ctx.params.e("backLayer") == 1;
|
||||
let jitter = ctx.params.f("timingRandomness").clamp(0.0, 1.0);
|
||||
let seed = ctx.params.f("randomSeed") as u32 ^ 0xca7d;
|
||||
let cols = if pr.e("rowsAndColumns") == 1 { rows } else { pr.f("columns").round().clamp(1.0, 1000.0) as i64 };
|
||||
let card_scale = pr.f("cardScale").max(0.01);
|
||||
let axis = pr.e("flipAxis");
|
||||
let direction = pr.e("flipDirection");
|
||||
let order = pr.e("flipOrder");
|
||||
let jitter = pr.f("timingRandomness").clamp(0.0, 1.0);
|
||||
let seed = pr.f("randomSeed") as u32 ^ 0xca7d;
|
||||
let (x0, y0, w, h) = layer_rect(ctx, &b);
|
||||
let (cw, ch) = (w / cols as f64, h / rows as f64);
|
||||
let nx = |i: i64| if cols > 1 { i as f64 / (cols - 1) as f64 } else { 0.0 };
|
||||
@@ -187,64 +214,91 @@ fn card_wipe(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
Some(o) => place_layer(ctx, &b, &o, 2),
|
||||
None => b.img.clone(),
|
||||
});
|
||||
let src = b.img.clone();
|
||||
b.img = remap(&src, false, |x, y| {
|
||||
let (lx, ly) = (x - x0, y - y0);
|
||||
if lx < 0.0 || ly < 0.0 || lx >= w || ly >= h {
|
||||
return Some((x, y));
|
||||
}
|
||||
let i = ((lx / cw).floor() as i64).min(cols - 1);
|
||||
let j = ((ly / ch).floor() as i64).min(rows - 1);
|
||||
let (ccx, ccy) = (x0 + (i as f64 + 0.5) * cw, y0 + (j as f64 + 0.5) * ch);
|
||||
let mut o = match order {
|
||||
1 => 1.0 - nx(i),
|
||||
2 => ny(j),
|
||||
3 => 1.0 - ny(j),
|
||||
4 => (nx(i) + ny(j)) * 0.5,
|
||||
5 => (1.0 - nx(i) + ny(j)) * 0.5,
|
||||
6 => (nx(i) + 1.0 - ny(j)) * 0.5,
|
||||
7 => 1.0 - (nx(i) + ny(j)) * 0.5,
|
||||
8 => {
|
||||
let g = grad.as_ref().map(|g| g.get_clamped(ccx as i64, ccy as i64)).unwrap_or([0.0; 4]);
|
||||
let (c, _) = unpremul(g);
|
||||
luminance(c[0], c[1], c[2]).clamp(0.0, 1.0) as f64
|
||||
// Back Layer: a chosen layer (stretched to fit), or this layer for projects saved with the
|
||||
// old "Self" choice; none hides the cards' backs.
|
||||
let back = match ctx.layer_param("backLayer", true) {
|
||||
Some(o) => Some(crate::util::fit_layer(ctx, &b, &o, true)),
|
||||
None if pr.b("backSelf") => Some(b.img.clone()),
|
||||
None => None,
|
||||
};
|
||||
let cam = crate::card3d::projection(ctx, &b);
|
||||
let eye = cam.eye();
|
||||
let light = crate::card3d::Lighting::from(ctx, &b);
|
||||
let t = ctx.time;
|
||||
let jit = |i: i64, j: i64, k: u32, speed: f64| -> f64 {
|
||||
let ph = hash1(i as u32, j as u32, seed ^ k) as f64 * std::f64::consts::TAU;
|
||||
let rate = 0.5 + hash1(i as u32, j as u32, seed ^ (k + 101)) as f64;
|
||||
(t * speed * rate * std::f64::consts::TAU + ph).sin()
|
||||
};
|
||||
let (jx, jy, jz, js) =
|
||||
(f("positionJitter/xJitterAmount"), f("positionJitter/yJitterAmount"), f("positionJitter/zJitterAmount"), f("positionJitter/jitterSpeed"));
|
||||
let (rjx, rjy, rjz, rjs) =
|
||||
(f("rotationJitter/xRotJitterAmount"), f("rotationJitter/yRotJitterAmount"), f("rotationJitter/zRotJitterAmount"), f("rotationJitter/rotJitterSpeed"));
|
||||
let mut pieces: Vec<Piece> = (0..rows * cols)
|
||||
.into_par_iter()
|
||||
.filter_map(|k| {
|
||||
let (i, j) = (k % cols, k / cols);
|
||||
let (ccx, ccy) = (x0 + (i as f64 + 0.5) * cw, y0 + (j as f64 + 0.5) * ch);
|
||||
let mut o = match order {
|
||||
1 => 1.0 - nx(i),
|
||||
2 => ny(j),
|
||||
3 => 1.0 - ny(j),
|
||||
4 => (nx(i) + ny(j)) * 0.5,
|
||||
5 => (1.0 - nx(i) + ny(j)) * 0.5,
|
||||
6 => (nx(i) + 1.0 - ny(j)) * 0.5,
|
||||
7 => 1.0 - (nx(i) + ny(j)) * 0.5,
|
||||
8 => {
|
||||
let g = grad.as_ref().map(|g| g.get_clamped(ccx as i64, ccy as i64)).unwrap_or([0.0; 4]);
|
||||
let (c, _) = unpremul(g);
|
||||
luminance(c[0], c[1], c[2]).clamp(0.0, 1.0) as f64
|
||||
}
|
||||
_ => nx(i),
|
||||
};
|
||||
if jitter > 0.0 {
|
||||
o += (hash1(i as u32, j as u32, seed ^ 0x7177) as f64 - o) * jitter;
|
||||
}
|
||||
_ => nx(i),
|
||||
};
|
||||
if jitter > 0.0 {
|
||||
o += (hash1(i as u32, j as u32, seed ^ 0x7177) as f64 - o) * jitter;
|
||||
}
|
||||
let p = ((done - o * (1.0 - tw)) / tw).clamp(0.0, 1.0);
|
||||
// Card Scale: each card is scaled about its centre (gaps below 1, cropped above 1).
|
||||
let (mut dx, mut dy) = ((x - ccx) / card_scale, (y - ccy) / card_scale);
|
||||
if dx.abs() > cw * 0.5 + 1e-9 || dy.abs() > ch * 0.5 + 1e-9 {
|
||||
return None;
|
||||
}
|
||||
if p <= 0.0 {
|
||||
return Some((ccx + dx, ccy + dy));
|
||||
}
|
||||
let cos = (p * std::f64::consts::PI).cos();
|
||||
if cos.abs() < 1e-4 || (cos < 0.0 && !back_self) {
|
||||
return None;
|
||||
}
|
||||
let around_x = match axis {
|
||||
1 => false,
|
||||
2 => hash1(i as u32, j as u32, seed ^ 0x55) < 0.5,
|
||||
_ => true,
|
||||
};
|
||||
if around_x {
|
||||
dy /= cos;
|
||||
if dy.abs() > ch * 0.5 {
|
||||
let p = ((done - o * (1.0 - tw)) / tw).clamp(0.0, 1.0);
|
||||
let around_x = match axis {
|
||||
1 => false,
|
||||
2 => hash1(i as u32, j as u32, seed ^ 0x55) < 0.5,
|
||||
_ => true,
|
||||
};
|
||||
let sign = match direction {
|
||||
1 => -1.0,
|
||||
2 if hash1(i as u32, j as u32, seed ^ 0x99) < 0.5 => -1.0,
|
||||
_ => 1.0,
|
||||
};
|
||||
let a = p * std::f64::consts::PI * sign;
|
||||
let flip = if around_x { rot_axis([1.0, 0.0, 0.0], a) } else { rot_axis([0.0, 1.0, 0.0], a) };
|
||||
let jr = crate::card3d::rot_ordered(
|
||||
(rjx * jit(i, j, 11, rjs)).to_radians(),
|
||||
(rjy * jit(i, j, 12, rjs)).to_radians(),
|
||||
(rjz * jit(i, j, 13, rjs)).to_radians(),
|
||||
0,
|
||||
);
|
||||
let r = crate::card3d::mat_mul(&jr, &flip);
|
||||
let pos = [ccx + jx * jit(i, j, 21, js) * cw, ccy + jy * jit(i, j, 22, js) * ch, jz * jit(i, j, 23, js) * cw.max(ch)];
|
||||
// Card Scale: each card is scaled about its centre (gaps below 1, cropped above 1).
|
||||
let (sw, sh) = ((cw * 0.5 * card_scale.min(1.0)) as f32, (ch * 0.5 * card_scale.min(1.0)) as f32);
|
||||
let (cx, cy) = (ccx as f32, ccy as f32);
|
||||
let poly = [[cx - sw, cy - sh], [cx + sw, cy - sh], [cx + sw, cy + sh], [cx - sw, cy + sh]];
|
||||
// Above 1 the card's picture is magnified (cropped to the card).
|
||||
let k = card_scale.max(1.0);
|
||||
let rs: crate::card3d::M3 = std::array::from_fn(|q| [r[q][0] * k, r[q][1] * k, r[q][2]]);
|
||||
let poly = poly.map(|v| [cx + (v[0] - cx) / k as f32, cy + (v[1] - cy) / k as f32]);
|
||||
let mut pc = Piece::new(&poly, [cx, cy], &rs, pos, cam)?;
|
||||
if pc.back && back.is_none() {
|
||||
return None;
|
||||
}
|
||||
} else {
|
||||
dx /= cos;
|
||||
if dx.abs() > cw * 0.5 {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
Some((ccx + dx, ccy + dy))
|
||||
});
|
||||
pc.back_mirror = if around_x { 1 } else { 2 };
|
||||
pc.light(&light, &r, pos, eye);
|
||||
Some(pc)
|
||||
})
|
||||
.collect();
|
||||
sort_far_first(&mut pieces);
|
||||
let mut out = effectcraft_raster::Image::new(b.img.width, b.img.height);
|
||||
draw_pieces(&mut out, &pieces, &b.img, back.as_ref());
|
||||
b.img = out;
|
||||
b
|
||||
}
|
||||
|
||||
@@ -447,12 +501,15 @@ pub fn specs() -> Vec<EffectSpec> {
|
||||
vec![
|
||||
done(),
|
||||
p("transitionWidth", "Transition Width", num(50.0), slider(1.0, 100.0, 1.0, 100.0, 0)),
|
||||
p("backLayer", "Back Layer", Value::Enum(0), popup(&["None", "Self"])),
|
||||
p("backLayer", "Back Layer", Value::Layer(None), ParamUi::Layer),
|
||||
// Projects saved when Back Layer was a None / Self popup: Self.
|
||||
p("backSelf", "Back Layer Is Self", Value::Bool(false), ParamUi::Hidden),
|
||||
p("rowsAndColumns", "Rows & Columns", Value::Enum(0), popup(&["Independent", "Columns Follows Rows"])),
|
||||
p("rows", "Rows", num(8.0), slider(1.0, 1000.0, 1.0, 100.0, 0)),
|
||||
p("columns", "Columns", num(8.0), slider(1.0, 1000.0, 1.0, 100.0, 0)),
|
||||
p("cardScale", "Card Scale", num(1.0), slider(0.01, 10.0, 0.01, 2.0, 2)),
|
||||
p("flipAxis", "Flip Axis", Value::Enum(0), popup(&["X", "Y", "Random"])),
|
||||
p("flipDirection", "Flip Direction", Value::Enum(0), popup(&["Positive", "Negative", "Random"])),
|
||||
p(
|
||||
"flipOrder",
|
||||
"Flip Order",
|
||||
@@ -472,7 +529,22 @@ pub fn specs() -> Vec<EffectSpec> {
|
||||
p("gradientLayer", "Gradient Layer", Value::Layer(None), ParamUi::Layer),
|
||||
p("timingRandomness", "Timing Randomness", num(0.0), slider(0.0, 1.0, 0.0, 1.0, 2)),
|
||||
p("randomSeed", "Random Seed", num(1.0), slider(0.0, 10000.0, 0.0, 100.0, 0)),
|
||||
],
|
||||
]
|
||||
.into_iter()
|
||||
.chain(crate::card3d::camera_params(2.0))
|
||||
.chain(crate::card3d::lighting_params(1.0, 0.25))
|
||||
.chain(crate::card3d::material_params(0.75))
|
||||
.chain([
|
||||
p("positionJitter/xJitterAmount", "X Jitter Amount", num(0.0), slider(0.0, 100.0, 0.0, 5.0, 2)),
|
||||
p("positionJitter/jitterSpeed", "Jitter Speed", num(1.0), slider(0.0, 100.0, 0.0, 10.0, 2)),
|
||||
p("positionJitter/yJitterAmount", "Y Jitter Amount", num(0.0), slider(0.0, 100.0, 0.0, 5.0, 2)),
|
||||
p("positionJitter/zJitterAmount", "Z Jitter Amount", num(0.0), slider(0.0, 100.0, 0.0, 5.0, 2)),
|
||||
p("rotationJitter/xRotJitterAmount", "X Rot Jitter Amount", num(0.0), slider(0.0, 360.0, 0.0, 90.0, 1)),
|
||||
p("rotationJitter/rotJitterSpeed", "Rot Jitter Speed", num(1.0), slider(0.0, 100.0, 0.0, 10.0, 2)),
|
||||
p("rotationJitter/yRotJitterAmount", "Y Rot Jitter Amount", num(0.0), slider(0.0, 360.0, 0.0, 90.0, 1)),
|
||||
p("rotationJitter/zRotJitterAmount", "Z Rot Jitter Amount", num(0.0), slider(0.0, 360.0, 0.0, 90.0, 1)),
|
||||
])
|
||||
.collect(),
|
||||
card_wipe,
|
||||
),
|
||||
spec(
|
||||
@@ -635,7 +707,7 @@ mod tests {
|
||||
let back = run(
|
||||
"ec.transition.cardwipe",
|
||||
&img,
|
||||
&[("completion", num(50.0)), ("rows", num(1.0)), ("columns", num(8.0)), ("transitionWidth", num(10.0)), ("backLayer", Value::Enum(1))],
|
||||
&[("completion", num(50.0)), ("rows", num(1.0)), ("columns", num(8.0)), ("transitionWidth", num(10.0)), ("backSelf", Value::Bool(true))],
|
||||
);
|
||||
assert_eq!(back.get(2, 20)[3], 1.0);
|
||||
}
|
||||
@@ -746,4 +818,52 @@ mod tests {
|
||||
assert_eq!(out.get(50, 50)[3], 0.0, "centre tiles are gone first");
|
||||
assert_eq!(out.get(1, 1)[3], 1.0, "corners last");
|
||||
}
|
||||
|
||||
fn run_t(img: &Image, set: &[(&str, Value)], time: f64, host: Option<&dyn crate::EffectHost>) -> Image {
|
||||
let s = find("ec.transition.cardwipe").unwrap();
|
||||
let size = [img.width as f64, img.height as f64];
|
||||
let mut params = Params { values: s.params.iter().map(|p| (p.id.to_string(), crate::default_value(p, size))).collect() };
|
||||
for (k, v) in set {
|
||||
params.values.insert(k.to_string(), v.clone());
|
||||
}
|
||||
let env = crate::EffectEnv { host, ..Default::default() };
|
||||
let ctx = EffectCtx { params: ¶ms, time, layer_size: size, seed: 7, adjustment: false, env };
|
||||
(s.render)(&ctx, Buf { img: img.clone(), offset: [0.0, 0.0], scale: 1.0 }).img
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn card_wipe_back_layer_direction_jitter_and_camera() {
|
||||
struct Green;
|
||||
impl crate::EffectHost for Green {
|
||||
fn layer(&self, _: u64, _: bool) -> Option<crate::LayerPixels> {
|
||||
Some(crate::LayerPixels { buf: Buf { img: Image::filled(80, 40, [0.0, 1.0, 0.0, 1.0]), offset: [0.0; 2], scale: 1.0 }, size: [80.0, 40.0] })
|
||||
}
|
||||
fn audio(&self, _: u64, _: f64, _: usize, _: u32) -> Option<Vec<f32>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
let img = Image::filled(80, 40, [1.0, 0.0, 0.0, 1.0]);
|
||||
let v = [("completion", num(100.0)), ("rows", num(1.0)), ("columns", num(8.0))];
|
||||
// Fully flipped cards show the Back Layer.
|
||||
let mut b: Vec<(&str, Value)> = v.to_vec();
|
||||
b.push(("backLayer", Value::Layer(Some(3))));
|
||||
let out = run_t(&img, &b, 0.0, Some(&Green));
|
||||
let p = out.get(45, 20);
|
||||
assert!(p[1] > 0.95 && p[0] < 0.05, "{p:?}");
|
||||
// Half way, Flip Direction decides which way the cards turn (the views differ).
|
||||
let half = [("completion", num(30.0)), ("rows", num(1.0)), ("columns", num(1.0)), ("transitionWidth", num(100.0)), ("flipAxis", Value::Enum(1))];
|
||||
let pos = run_t(&img, &[half.as_slice(), &[("cameraPosition/xRotation", num(20.0))]].concat(), 0.0, None);
|
||||
let neg = run_t(&img, &[half.as_slice(), &[("cameraPosition/xRotation", num(20.0)), ("flipDirection", Value::Enum(1))]].concat(), 0.0, None);
|
||||
assert_ne!(pos, neg);
|
||||
// Position Jitter moves the cards over time (and only then).
|
||||
let j = [("completion", num(0.0)), ("positionJitter/xJitterAmount", num(0.3))];
|
||||
let a = run_t(&img, &j, 0.0, None);
|
||||
let c = run_t(&img, &j, 0.37, None);
|
||||
assert_ne!(a, c);
|
||||
assert_eq!(c, run_t(&img, &j, 0.37, None), "deterministic");
|
||||
assert_eq!(run_t(&img, &[("completion", num(0.0))], 0.37, None), img, "no jitter, no change");
|
||||
// Camera Position rotation tilts the whole wipe in perspective.
|
||||
let tilt = run_t(&img, &[("completion", num(0.0)), ("cameraPosition/yRotation", num(40.0))], 0.0, None);
|
||||
assert_eq!(tilt.get(0, 0)[3], 0.0);
|
||||
}
|
||||
}
|
||||
@@ -151,6 +151,31 @@ impl EffectHost for FxHost<'_, '_, '_> {
|
||||
}
|
||||
}
|
||||
|
||||
fn comp_scene(&self) -> Option<effectcraft_effects::CompScene> {
|
||||
// The comp camera's view of the layer (as if 3D at its transform), brought back into the
|
||||
// layer's own pixel grid through the inverse of how the layer itself composites.
|
||||
let world = self.ctx.world_matrix(self.layer);
|
||||
let (cam, _, _) = self.ctx.camera();
|
||||
let p = (cam * world).0;
|
||||
let back = self.ctx.layer_to_comp(self.layer).0.inverse()?.0;
|
||||
let rows = [p[0], p[1], p[3]];
|
||||
let camera: [[f64; 4]; 3] = std::array::from_fn(|i| std::array::from_fn(|j| (0..3).map(|k| back[i][k] * rows[k][j]).sum()));
|
||||
let inv = world.inverse();
|
||||
let light = three_d::light::lights_at(self.ctx).first().and_then(|l| {
|
||||
let inv = inv.as_ref()?;
|
||||
let pos = inv.apply(l.pos);
|
||||
let dir = inv.apply_vec(l.dir);
|
||||
let len = (dir.x * dir.x + dir.y * dir.y + dir.z * dir.z).sqrt().max(1e-12);
|
||||
let kind = match l.kind {
|
||||
effectcraft_project::LightKind::Parallel => 0,
|
||||
effectcraft_project::LightKind::Ambient => 2,
|
||||
_ => 1,
|
||||
};
|
||||
Some(effectcraft_effects::CompLight { pos: [pos.x, pos.y, pos.z], dir: [dir.x / len, dir.y / len, dir.z / len], color: l.color, kind })
|
||||
});
|
||||
Some(effectcraft_effects::CompScene { camera: Some(camera), light })
|
||||
}
|
||||
|
||||
fn layer_at(&self, id: u64, comp_time: f64, masks_and_effects: bool) -> Option<LayerPixels> {
|
||||
let other = self.ctx.layer(effectcraft_project::LayerId(id))?;
|
||||
if other.id == self.layer.id || self.r.depth > MAX_FX_DEPTH {
|
||||
|
||||
@@ -163,6 +163,19 @@ fn effects_run_in_pipeline() {
|
||||
assert!(c[1] > 0.99 && c[0] < 0.01, "{c:?}");
|
||||
}
|
||||
|
||||
/// Card Dance's Comp Camera through the default comp camera reproduces a 2D layer in place.
|
||||
#[test]
|
||||
fn card_dance_comp_camera_matches_the_default_view() {
|
||||
let (mut p, cid, comp) = setup();
|
||||
let mut l = solid(&mut p, &comp, [0.2, 0.6, 1.0], 50, 50);
|
||||
add_effect(&mut p, &mut l, "ec.sim.carddance", [50.0, 50.0], &[("cameraSystem", Value::Enum(2))]);
|
||||
p.comp_mut(cid).unwrap().layers.push(l);
|
||||
let img = render_frame(&p, cid, Tick::ZERO, 1.0);
|
||||
let c = img.get(100, 50);
|
||||
assert!((c[0] - 0.2).abs() < 0.02 && (c[2] - 1.0).abs() < 0.02 && c[3] > 0.99, "{c:?}");
|
||||
assert!(img.get(10, 50)[3] < 0.01);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------ layer cache
|
||||
|
||||
fn render_cached(p: &Project, cid: ItemId, t: Tick, cache: Option<&crate::LayerCache>) -> crate::Image {
|
||||
|
||||
+5
-5
@@ -3,11 +3,11 @@
|
||||
<!-- Generated from the effect registry (crates/effects/src/catalog.rs); do not edit by hand.
|
||||
Regenerate: UPDATE_DOCS=1 cargo test -p effectcraft-effects --lib effects_doc_is_current -->
|
||||
|
||||
EffectCraft ships 303 effects with 2562 parameters in total, grouped into the same categories as the Effects & Presets panel. Every effect is our own implementation, written from public behaviour descriptions and standard image-processing literature. Parameter names, order, popup options, units, defaults and ranges follow the reference application so that projects, expressions and muscle memory carry over.
|
||||
EffectCraft ships 303 effects with 2647 parameters in total, grouped into the same categories as the Effects & Presets panel. Every effect is our own implementation, written from public behaviour descriptions and standard image-processing literature. Parameter names, order, popup options, units, defaults and ranges follow the reference application so that projects, expressions and muscle memory carry over.
|
||||
|
||||
- **GPU**: 24 effects also run on the GPU compositor with identical results.
|
||||
- **32**: 303 effects process 32-bit float (HDR, overbright) pixels without clamping.
|
||||
- **Status**: 266 implemented in full, 37 partial (what is missing is listed).
|
||||
- **Status**: 269 implemented in full, 34 partial (what is missing is listed).
|
||||
|
||||
Effects are addressed by id (`ec.<category>.<name>`) or display name in commands, scripts and the control channel, and every parameter by its id (`effects/#1/blurriness`).
|
||||
|
||||
@@ -328,11 +328,11 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
||||
| CC Scatterize | `ec.sim.ccscatterize` | 4 | | 32 | Implemented |
|
||||
| CC Snowfall | `ec.sim.ccsnowfall` | 19 | | 32 | Implemented |
|
||||
| CC Star Burst | `ec.sim.ccstarburst` | 6 | | 32 | Implemented |
|
||||
| Card Dance | `ec.sim.carddance` | 35 | | 32 | Partial: no camera system, corner pins or lighting/material controls |
|
||||
| Card Dance | `ec.sim.carddance` | 53 | | 32 | Implemented |
|
||||
| Caustics | `ec.sim.caustics` | 21 | | 32 | Partial: no Sky group or light type |
|
||||
| Foam | `ec.sim.foam` | 24 | | 32 | Partial: no wobble, pop velocity, custom bubble texture, environment map or flow map |
|
||||
| Particle Playground | `ec.sim.particleplayground` | 37 | | 32 | Partial: no Particle Exploder, Ephemeral Property Mapper, Affects groups or text particles |
|
||||
| Shatter | `ec.sim.shatter` | 26 | | 32 | Partial: no custom shatter map, gradient, textures or extrusion rendering |
|
||||
| Shatter | `ec.sim.shatter` | 60 | | 32 | Implemented |
|
||||
| Wave World | `ec.sim.waveworld` | 27 | | 32 | Partial: no wireframe controls, dry-area rendering or ground group |
|
||||
|
||||
## Stylize
|
||||
@@ -400,7 +400,7 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
||||
| CC Scale Wipe | `ec.transition.ccscalewipe` | 3 | | 32 | Implemented |
|
||||
| CC Twister | `ec.transition.cctwister` | 5 | | 32 | Implemented |
|
||||
| CC WarpoMatic | `ec.transition.ccwarpomatic` | 8 | | 32 | Implemented |
|
||||
| Card Wipe | `ec.transition.cardwipe` | 12 | | 32 | Partial: 2D flip only; no back layer picker, camera, lighting or jitter |
|
||||
| Card Wipe | `ec.transition.cardwipe` | 44 | | 32 | Implemented |
|
||||
| Gradient Wipe | `ec.transition.gradientwipe` | 5 | | 32 | Implemented |
|
||||
| Iris Wipe | `ec.transition.iriswipe` | 7 | | 32 | Implemented |
|
||||
| Linear Wipe | `ec.transition.linearwipe` | 3 | | 32 | Implemented |
|
||||
|
||||
Reference in new issue
Block a user