mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-30 13:08:07 -06:00
G1: Turbulent Displace Evolution changes the output (#943)
Turbulent Displace read its noise through value_noise, whose quintic fade also eased the evolution axis. Near every whole revolution the pattern hardly moved: Evolution 0 and 37 degrees (or 360 and 400) differed by well under a hundredth of the frame, so different Evolution values looked identical, and the motion sped up and slowed down within each revolution (the #1816 symptom). Add value_noise_linear_z (x and y keep the fade, z is linear) on the CPU and in the WGSL kernels and use it for Turbulent Displace on both paths. value_noise is unchanged, so the other effects and the pinned simulation goldens are unaffected. Tests: CPU test that non-equivalent Evolution values (fractional, past 360, past 720) all differ and that Cycle Evolution still matches 0/360; unit test of the linear z axis; GPU-vs-CPU cases at small and large Evolution. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FYXhNN7W7nB6sKjwwU7CDM
This commit is contained in:
@@ -19,7 +19,7 @@ use effectcraft_project::ParamUi;
|
||||
use effectcraft_raster::{Image, Px};
|
||||
use rayon::prelude::*;
|
||||
|
||||
use crate::generate::value_noise;
|
||||
use crate::generate::value_noise_linear_z;
|
||||
use crate::util::{SRC_NAMES, Src, fit_layer, layer_cover_pad, layer_rect, lerp4, map_xy, pick, premul, remap, smoothstep, src_at, unpremul};
|
||||
use crate::{Buf, EffectCtx, EffectSpec, col, num, p, popup, slider};
|
||||
|
||||
@@ -37,13 +37,15 @@ fn pct(max: f64) -> ParamUi {
|
||||
|
||||
// ---------------------------------------------------------------- Turbulent Displace
|
||||
|
||||
/// Fractional-octave fBm of value noise in 0..1. Evolution (`z`) is linear: with the eased z of
|
||||
/// plain value noise a few degrees of Evolution barely moved the pattern (#943).
|
||||
fn fbm(u: f64, v: f64, z: f64, seed: u32, octaves: f64, falloff: f32) -> f32 {
|
||||
let n = octaves.ceil().max(1.0) as usize;
|
||||
let frac = (octaves - octaves.floor()) as f32;
|
||||
let (mut sum, mut norm, mut amp, mut f) = (0.0f32, 0.0f32, 1.0f32, 1.0f32);
|
||||
for o in 0..n {
|
||||
let w = if o + 1 == n && frac > 0.0 { frac } else { 1.0 };
|
||||
sum += value_noise(u as f32 * f, v as f32 * f, z as f32 + o as f32 * 5.17, seed.wrapping_add(o as u32 * 101)) * amp * w;
|
||||
sum += value_noise_linear_z(u as f32 * f, v as f32 * f, z as f32 + o as f32 * 5.17, seed.wrapping_add(o as u32 * 101)) * amp * w;
|
||||
norm += amp * w;
|
||||
amp *= falloff;
|
||||
f *= 2.0;
|
||||
@@ -1115,6 +1117,45 @@ mod tests {
|
||||
assert_eq!(TURBULENT_PINNING[11], "Pin Left Locked");
|
||||
}
|
||||
|
||||
/// #943: every non-equivalent Evolution value renders a visibly different frame. The eased
|
||||
/// evolution axis left 0 and 37 degrees (or 360 and 400) within a hundredth of each other.
|
||||
#[test]
|
||||
fn turbulent_displace_evolution_changes_the_output() {
|
||||
// A smooth ramp (red = x, green = y) with Pin All Locked, so the frame stays opaque and
|
||||
// the difference is purely how far the pattern moved.
|
||||
let mut ramp = Image::new(64, 64);
|
||||
for y in 0..64u32 {
|
||||
for x in 0..64u32 {
|
||||
ramp.set(x, y, [x as f32 / 63.0, y as f32 / 63.0, 0.25, 1.0]);
|
||||
}
|
||||
}
|
||||
let render = |evo: f64, extra: &[(&str, Value)]| {
|
||||
let base = [("evolution", num(evo)), ("size", num(10.0)), ("pinning", Value::Enum(8))];
|
||||
run("ec.distort.turbulentdisplace", &[base.as_slice(), extra].concat(), ramp.clone()).img
|
||||
};
|
||||
let evolutions = [0.0, 37.0, 90.5, 180.0, 360.0, 400.25, 725.0, 1000.5];
|
||||
let frames: Vec<Image> = evolutions.iter().map(|&e| render(e, &[])).collect();
|
||||
for (i, a) in frames.iter().enumerate() {
|
||||
for (j, b) in frames.iter().enumerate().skip(i + 1) {
|
||||
let d = max_diff(a, b);
|
||||
assert!(d > 0.01, "evolution {} and {} differ by only {d}", evolutions[i], evolutions[j]);
|
||||
}
|
||||
}
|
||||
// Constant rate: 20 degrees next to a whole revolution (from either side) is a visible
|
||||
// step, not the rounding-error size the eased axis gave (under 0.001).
|
||||
assert!(max_diff(&frames[0], &render(20.0, &[])) > 0.02);
|
||||
assert!(max_diff(&render(340.0, &[]), &frames[4]) > 0.02);
|
||||
// 0 and 360 degrees are different frames without Cycle Evolution, and the same frame with
|
||||
// Cycle (in Revolutions) = 1, at any starting angle.
|
||||
assert!(max_diff(&frames[0], &frames[4]) > 0.01);
|
||||
let cycle = [("evolutionOptions/cycleEvolution", Value::Bool(true)), ("evolutionOptions/cycle", num(1.0))];
|
||||
assert!(max_diff(&render(0.0, &cycle), &render(360.0, &cycle)) < 1e-4);
|
||||
assert!(max_diff(&render(37.0, &cycle), &render(397.0, &cycle)) < 1e-4);
|
||||
assert!(max_diff(&render(0.0, &cycle), &render(37.0, &cycle)) > 0.01);
|
||||
// Random Seed still gives a different pattern at the same Evolution.
|
||||
assert!(max_diff(&frames[1], &render(37.0, &[("evolutionOptions/randomSeed", num(5.0))])) > 0.01);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn magnify_blending_none_and_scaling() {
|
||||
let img = test_img(32, 32);
|
||||
|
||||
@@ -292,10 +292,22 @@ pub(crate) fn fade(t: f32) -> f32 {
|
||||
t * t * t * (t * (t * 6.0 - 15.0) + 10.0)
|
||||
}
|
||||
|
||||
/// 3D value noise in 0..1 (z = evolution).
|
||||
/// 3D value noise in 0..1 (z = evolution). The quintic fade applies on all three axes, so z
|
||||
/// eases in and out of every integer.
|
||||
pub fn value_noise(x: f32, y: f32, z: f32, seed: u32) -> f32 {
|
||||
value_noise_z(x, y, z, seed, true)
|
||||
}
|
||||
|
||||
/// [`value_noise`] with the evolution axis z interpolated linearly (x and y keep the quintic
|
||||
/// fade). The fade's zero slope at every integer z made a small Evolution change almost invisible
|
||||
/// next to a whole revolution (#943, #1816); linear z moves the pattern at a constant rate.
|
||||
pub fn value_noise_linear_z(x: f32, y: f32, z: f32, seed: u32) -> f32 {
|
||||
value_noise_z(x, y, z, seed, false)
|
||||
}
|
||||
|
||||
fn value_noise_z(x: f32, y: f32, z: f32, seed: u32, ease_z: bool) -> f32 {
|
||||
let (x0, y0, z0) = (x.floor(), y.floor(), z.floor());
|
||||
let (fx, fy, fz) = (fade(x - x0), fade(y - y0), fade(z - z0));
|
||||
let (fx, fy, fz) = (fade(x - x0), fade(y - y0), if ease_z { fade(z - z0) } else { z - z0 });
|
||||
let (ix, iy, iz) = (x0 as i32, y0 as i32, z0 as i32);
|
||||
let l = |a: f32, b: f32, t: f32| a + (b - a) * t;
|
||||
let c = |dx: i32, dy: i32, dz: i32| lattice(ix + dx, iy + dy, iz + dz, seed);
|
||||
@@ -414,6 +426,27 @@ mod generator_tests {
|
||||
Image::filled(32, 32, [1.0, 0.0, 0.0, 1.0])
|
||||
}
|
||||
|
||||
/// #943: the evolution axis is linear, so z moves the noise at a constant rate, while the
|
||||
/// classic `value_noise` keeps its eased z (other effects and their pinned goldens use it).
|
||||
#[test]
|
||||
fn linear_z_value_noise_moves_at_a_constant_rate() {
|
||||
for (x, y, seed) in [(0.3f32, 0.7f32, 5u32), (2.4, -1.6, 11), (-3.1, 4.9, 0)] {
|
||||
let a = value_noise_linear_z(x, y, 0.0, seed);
|
||||
let b = value_noise_linear_z(x, y, 1.0, seed);
|
||||
for z in [0.1f32, 0.25, 0.5, 0.75, 0.9] {
|
||||
let want = a + (b - a) * z;
|
||||
let got = value_noise_linear_z(x, y, z, seed);
|
||||
assert!((got - want).abs() < 1e-5, "({x}, {y}) z {z}: {got} vs {want}");
|
||||
}
|
||||
// Same lattice values at whole z, and x / y are untouched.
|
||||
assert_eq!(value_noise_linear_z(x, y, 2.0, seed), value_noise(x, y, 2.0, seed));
|
||||
}
|
||||
// The eased version still has the old shape: at z = 0.25 it lags the linear one.
|
||||
let (a, b) = (value_noise(0.3, 0.7, 0.0, 5), value_noise(0.3, 0.7, 1.0, 5));
|
||||
let eased = value_noise(0.3, 0.7, 0.25, 5);
|
||||
assert!((eased - (a + (b - a) * 0.25)).abs() > 1e-3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gradient_ramp_stays_inside_the_layer_alpha() {
|
||||
// #1063: a shape's transparent pixels stay transparent; the ramp fills only the shape.
|
||||
|
||||
@@ -238,7 +238,7 @@ fn dst_fbm(u: f32, v: f32, z: f32, seed: u32) -> f32 {
|
||||
if (o + 1u == n && frac > 0.0) {
|
||||
w = frac;
|
||||
}
|
||||
sum += value_noise(u * f, v * f, z + f32(o) * 5.17, seed + o * 101u) * amp * w;
|
||||
sum += value_noise_linear_z(u * f, v * f, z + f32(o) * 5.17, seed + o * 101u) * amp * w;
|
||||
norm += amp * w;
|
||||
amp *= falloff;
|
||||
f *= 2.0;
|
||||
|
||||
@@ -509,12 +509,25 @@ fn fade(t: f32) -> f32 {
|
||||
}
|
||||
|
||||
fn value_noise(x: f32, y: f32, z: f32, seed: u32) -> f32 {
|
||||
return value_noise_z(x, y, z, seed, true);
|
||||
}
|
||||
|
||||
// effects::generate::value_noise_linear_z: z (evolution) interpolates linearly, x and y keep the
|
||||
// quintic fade (Turbulent Displace, #943).
|
||||
fn value_noise_linear_z(x: f32, y: f32, z: f32, seed: u32) -> f32 {
|
||||
return value_noise_z(x, y, z, seed, false);
|
||||
}
|
||||
|
||||
fn value_noise_z(x: f32, y: f32, z: f32, seed: u32, ease_z: bool) -> f32 {
|
||||
let x0 = floor(x);
|
||||
let y0 = floor(y);
|
||||
let z0 = floor(z);
|
||||
let fx = fade(x - x0);
|
||||
let fy = fade(y - y0);
|
||||
let fz = fade(z - z0);
|
||||
var fz = z - z0;
|
||||
if (ease_z) {
|
||||
fz = fade(fz);
|
||||
}
|
||||
let ix = i32(x0);
|
||||
let iy = i32(y0);
|
||||
let iz = i32(z0);
|
||||
|
||||
@@ -55,6 +55,11 @@ fn turbulent_displace_and_cc_lens() {
|
||||
],
|
||||
);
|
||||
effect_case("ec.distort.turbulentdisplace", &[("displacement", e(8)), ("size", n(15.0)), ("pinning", e(6))]);
|
||||
// #943: Evolution moves the pattern at a constant rate on both paths, including steps of a
|
||||
// few degrees and angles past a whole revolution, where an eased evolution axis diverges.
|
||||
for evolution in [10.0, 37.0, 400.0, 725.5] {
|
||||
effect_case("ec.distort.turbulentdisplace", &[("evolution", n(evolution)), ("size", n(12.0)), ("amount", n(30.0))]);
|
||||
}
|
||||
effect_case("ec.distort.cclens", &[("size", n(70.0))]);
|
||||
effect_case("ec.distort.cclens", &[("size", n(45.0)), ("convergence", n(60.0)), ("center", Value::Vec2([30.0, 20.0]))]);
|
||||
effect_case("ec.distort.cclens", &[("convergence", n(-70.0))]);
|
||||
|
||||
Reference in New Issue
Block a user