diff --git a/crates/effects/src/distort2.rs b/crates/effects/src/distort2.rs
--- a/crates/effects/src/distort2.rs
+++ b/crates/effects/src/distort2.rs
@@ -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);
diff --git a/crates/effects/src/generate.rs b/crates/effects/src/generate.rs
--- a/crates/effects/src/generate.rs
+++ b/crates/effects/src/generate.rs
@@ -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.
diff --git a/crates/gpu/src/shaders/fx_distort.wgsl b/crates/gpu/src/shaders/fx_distort.wgsl
--- a/crates/gpu/src/shaders/fx_distort.wgsl
+++ b/crates/gpu/src/shaders/fx_distort.wgsl
@@ -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;
diff --git a/crates/gpu/src/shaders/kernels.wgsl b/crates/gpu/src/shaders/kernels.wgsl
--- a/crates/gpu/src/shaders/kernels.wgsl
+++ b/crates/gpu/src/shaders/kernels.wgsl
@@ -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);
diff --git a/crates/gpu/src/tests/fx_distort.rs b/crates/gpu/src/tests/fx_distort.rs
--- a/crates/gpu/src/tests/fx_distort.rs
+++ b/crates/gpu/src/tests/fx_distort.rs
@@ -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))]);
