diff --git a/crates/effects/src/generate.rs b/crates/effects/src/generate.rs index 09b25a27..2b543352 100644 --- a/crates/effects/src/generate.rs +++ b/crates/effects/src/generate.rs @@ -262,7 +262,7 @@ fn circle(ctx: &EffectCtx, mut b: Buf) -> Buf { // ---- value noise / fBm (our own lattice noise with quintic fade) ---- #[inline] -fn lattice(ix: i32, iy: i32, iz: i32, seed: u32) -> f32 { +pub(crate) fn lattice(ix: i32, iy: i32, iz: i32, seed: u32) -> f32 { let mut h = (ix as u32).wrapping_mul(0x27d4_eb2d) ^ (iy as u32).wrapping_mul(0x1656_67b1) ^ (iz as u32).wrapping_mul(0x9e37_79b9) ^ seed.wrapping_mul(0x85eb_ca6b); h ^= h >> 15; @@ -274,7 +274,7 @@ fn lattice(ix: i32, iy: i32, iz: i32, seed: u32) -> f32 { } #[inline] -fn fade(t: f32) -> f32 { +pub(crate) fn fade(t: f32) -> f32 { t * t * t * (t * (t * 6.0 - 15.0) + 10.0) } diff --git a/crates/effects/src/noise3.rs b/crates/effects/src/noise3.rs index 37bf853a..746943e6 100644 --- a/crates/effects/src/noise3.rs +++ b/crates/effects/src/noise3.rs @@ -4,7 +4,7 @@ //! //! Each "noise layer" (octave) is 3D value noise (x, y, evolution) on an integer lattice. The //! Noise Type picks the interpolation between lattice values (Block = nearest, Linear, -//! Soft Linear = quintic fade, Spline = Catmull-Rom). Successive layers are scaled by Sub +//! Soft Linear = quadratic B-spline, Spline = Catmull-Rom). Successive layers are scaled by Sub //! Scaling, weighted by Sub Influence, rotated by Sub Rotation and shifted by Sub Offset, and the //! Fractal Type shapes each layer (turbulent types fold the signed noise; dynamic types warp the //! domain; Max keeps the strongest layer; Strings keeps thin lines). Contrast / Brightness / @@ -17,7 +17,7 @@ use effectcraft_keyframe::Value; use effectcraft_project::ParamUi; use rayon::prelude::*; -use crate::generate::value_noise; +use crate::generate::{fade, lattice}; use crate::{Buf, EffectCtx, EffectSpec, ParamSpec, num, p, popup, slider}; /// Fractal Type options (shared by both effects). @@ -120,36 +120,36 @@ pub fn specs() -> Vec { // ---------------------------------------------------------------- noise kernels -fn lattice(ix: i32, iy: i32, iz: i32, seed: u32) -> f32 { - // The same hash as `generate::value_noise`, so Soft Linear matches it bit for bit. - let mut h = - (ix as u32).wrapping_mul(0x27d4_eb2d) ^ (iy as u32).wrapping_mul(0x1656_67b1) ^ (iz as u32).wrapping_mul(0x9e37_79b9) ^ seed.wrapping_mul(0x85eb_ca6b); - h ^= h >> 15; - h = h.wrapping_mul(0x2c1b_3c6d); - h ^= h >> 12; - h = h.wrapping_mul(0x297a_2d39); - h ^= h >> 15; - (h & 0xffff) as f32 / 65535.0 -} - -#[inline] -fn fade(t: f32) -> f32 { - t * t * t * (t * (t * 6.0 - 15.0) + 10.0) -} +/// Soft Linear's gain about mid-grey. Averaging 3 × 3 lattice values leaves the B-spline 0.55² of +/// their variance, against 0.784² (181/231 per axis) for the quintic fade Soft Linear used before +/// (#259); the gain keeps the noise's contrast. +const SOFT_LINEAR_GAIN: f32 = (181.0 / 231.0) / 0.55; #[inline] fn catmull(p0: f32, p1: f32, p2: f32, p3: f32, t: f32) -> f32 { 0.5 * (2.0 * p1 + (p2 - p0) * t + (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t * t + (3.0 * p1 - p0 - 3.0 * p2 + p3) * t * t * t) } +/// Uniform quadratic B-spline weights of the lattice values before, at and after the nearest one, +/// for `t` = position − nearest + 0.5 in 0..1. +#[inline] +fn bspline(t: f32) -> [f32; 3] { + let s = 1.0 - t; + [0.5 * s * s, 0.5 + t * s, 0.5 * t * t] +} + /// 3D value noise in about 0..1 with the given [`NOISE_TYPES`] interpolation in x and y (the /// evolution axis z always fades smoothly). +/// +/// Soft Linear is linear interpolation softened by a one-cell box filter, which is the quadratic +/// B-spline: smooth like Spline from 9 lattice values instead of 16. A fade between two values +/// has zero slope across every lattice line, which the turbulent types and high contrast turned +/// into a visible grid (#259); the B-spline's slope there is half the neighbours' difference. pub fn typed_noise(x: f32, y: f32, z: f32, seed: u32, noise_type: u32) -> f32 { - if noise_type == NOISE_SOFT_LINEAR { - return value_noise(x, y, z, seed); - } - let (x0, y0, z0) = (x.floor(), y.floor(), z.floor()); - let (tx, ty, fz) = (x - x0, y - y0, fade(z - z0)); + // Soft Linear's taps are centred on the nearest lattice point, the others start at the cell's. + let h = if noise_type == NOISE_SOFT_LINEAR { 0.5 } else { 0.0 }; + let (x0, y0, z0) = ((x + h).floor(), (y + h).floor(), z.floor()); + let (tx, ty, fz) = (x + h - x0, y + h - y0, fade(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 slice = |dz: i32| -> f32 { @@ -157,13 +157,19 @@ pub fn typed_noise(x: f32, y: f32, z: f32, seed: u32, noise_type: u32) -> f32 { match noise_type { 0 => c(0, 0), 1 => l(l(c(0, 0), c(1, 0), tx), l(c(0, 1), c(1, 1), tx), ty), + NOISE_SOFT_LINEAR => { + let (wx, wy) = (bspline(tx), bspline(ty)); + let row = |dy: i32| wx[0] * c(-1, dy) + wx[1] * c(0, dy) + wx[2] * c(1, dy); + wy[0] * row(-1) + wy[1] * row(0) + wy[2] * row(1) + } _ => { let row = |dy: i32| catmull(c(-1, dy), c(0, dy), c(1, dy), c(2, dy), tx); catmull(row(-1), row(0), row(1), row(2), ty) } } }; - l(slice(0), slice(1), fz) + let n = l(slice(0), slice(1), fz); + if noise_type == NOISE_SOFT_LINEAR { 0.5 + (n - 0.5) * SOFT_LINEAR_GAIN } else { n } } /// Everything the per-pixel evaluation needs, resolved from the parameters. diff --git a/crates/gpu/src/shaders/fx_noise.wgsl b/crates/gpu/src/shaders/fx_noise.wgsl index 28439067..c508973b 100644 --- a/crates/gpu/src/shaders/fx_noise.wgsl +++ b/crates/gpu/src/shaders/fx_noise.wgsl @@ -528,7 +528,7 @@ fn fxn_slice(ix: i32, iy: i32, iz: i32, tx: f32, ty: f32, seed: u32, nt: u32) -> // noise3::typed_noise. fn fxn_typed_noise(x: f32, y: f32, z: f32, seed: u32, nt: u32) -> f32 { if (nt == 2u) { - return value_noise(x, y, z, seed); + return soft_linear_noise(x, y, z, seed); } let x0 = floor(x); let y0 = floor(y); diff --git a/crates/gpu/src/shaders/kernels.wgsl b/crates/gpu/src/shaders/kernels.wgsl index 8576fdf9..1e1ad167 100644 --- a/crates/gpu/src/shaders/kernels.wgsl +++ b/crates/gpu/src/shaders/kernels.wgsl @@ -535,6 +535,36 @@ fn value_noise(x: f32, y: f32, z: f32, seed: u32) -> f32 { return a + (b - a) * fz; } +// Fractal Noise's Soft Linear (effects::noise3::typed_noise): the quadratic B-spline over the +// 3 × 3 lattice values around the nearest one, faded along evolution, and SOFT_LINEAR_GAIN. +const SOFT_LINEAR_GAIN: f32 = (181.0 / 231.0) / 0.55; + +fn bspline(t: f32) -> vec3 { + let s = 1.0 - t; + return vec3(0.5 * s * s, 0.5 + t * s, 0.5 * t * t); +} + +fn soft_linear_slice(ix: i32, iy: i32, iz: i32, wx: vec3, wy: vec3, seed: u32) -> f32 { + var sum = 0.0; + for (var j = 0; j < 3; j++) { + let y = iy + j - 1; + sum += wy[j] * (wx.x * lattice(ix - 1, y, iz, seed) + wx.y * lattice(ix, y, iz, seed) + wx.z * lattice(ix + 1, y, iz, seed)); + } + return sum; +} + +fn soft_linear_noise(x: f32, y: f32, z: f32, seed: u32) -> f32 { + let x0 = floor(x + 0.5); + let y0 = floor(y + 0.5); + let z0 = floor(z); + let wx = bspline(x + 0.5 - x0); + let wy = bspline(y + 0.5 - y0); + let a = soft_linear_slice(i32(x0), i32(y0), i32(z0), wx, wy, seed); + let b = soft_linear_slice(i32(x0), i32(y0), i32(z0) + 1, wx, wy, seed); + let n = a + (b - a) * fade(z - z0); + return 0.5 + (n - 0.5) * SOFT_LINEAR_GAIN; +} + // generate::put: blend a generated straight colour (alpha ga) over the pixel. fn put(px: vec4, g: vec3, ga: f32, blend_orig: f32) -> vec4 { let g2 = vec4(g * ga, ga); @@ -800,7 +830,7 @@ fn pointwise(@builtin(global_invocation_id) gid: vec3) { if (o + 1u == n_oct && frac > 0.0) { w = frac; } - var n = value_noise(u * f, v * f, evo + f32(o) * 7.31, seed + o); + var n = soft_linear_noise(u * f, v * f, evo + f32(o) * 7.31, seed + o); let s = n * 2.0 - 1.0; if (kind == 1u) { n = 1.0 - s * s;