mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-31 13:37:37 -06:00
M8: expressions engine (boa) with AE object model
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
808cd490cd
commit
4ce50e00a0
@@ -0,0 +1,109 @@
|
||||
//! Deterministic hashing, random numbers and smooth noise for `wiggle`, `random` and `noise`.
|
||||
//!
|
||||
//! Everything is a pure function of its seeds, so a frame renders identically on any thread,
|
||||
//! in any order, on native and on the web.
|
||||
|
||||
/// SplitMix64 finaliser: a good 64-bit mixing function.
|
||||
pub fn hash64(mut x: u64) -> u64 {
|
||||
x = x.wrapping_add(0x9e37_79b9_7f4a_7c15);
|
||||
x = (x ^ (x >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
|
||||
x = (x ^ (x >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
|
||||
x ^ (x >> 31)
|
||||
}
|
||||
|
||||
/// Combine several seeds into one.
|
||||
pub fn mix(parts: &[u64]) -> u64 {
|
||||
parts.iter().fold(0x2545_f491_4f6c_dd1d, |h, p| hash64(h ^ hash64(*p)))
|
||||
}
|
||||
|
||||
/// Uniform in `[0, 1)`.
|
||||
pub fn unit(seed: u64) -> f64 {
|
||||
(hash64(seed) >> 11) as f64 / (1u64 << 53) as f64
|
||||
}
|
||||
|
||||
/// Lattice value in `[-1, 1]`.
|
||||
fn lattice(seed: u64, i: i64) -> f64 {
|
||||
unit(seed ^ hash64(i as u64)) * 2.0 - 1.0
|
||||
}
|
||||
|
||||
fn fade(t: f64) -> f64 {
|
||||
t * t * t * (t * (t * 6.0 - 15.0) + 10.0)
|
||||
}
|
||||
|
||||
/// Smooth 1D value noise in `[-1, 1]` (C2-continuous, bounded by the lattice values).
|
||||
pub fn noise1(seed: u64, x: f64) -> f64 {
|
||||
if !x.is_finite() {
|
||||
return 0.0;
|
||||
}
|
||||
let i = x.floor();
|
||||
let u = fade(x - i);
|
||||
let i = i as i64;
|
||||
let (a, b) = (lattice(seed, i), lattice(seed, i + 1));
|
||||
a + (b - a) * u
|
||||
}
|
||||
|
||||
/// Smooth 3D value noise in `[-1, 1]` (AE's `noise()`: nearby inputs give nearby outputs).
|
||||
pub fn noise3(x: f64, y: f64, z: f64) -> f64 {
|
||||
if !(x.is_finite() && y.is_finite() && z.is_finite()) {
|
||||
return 0.0;
|
||||
}
|
||||
let (fx, fy, fz) = (x.floor(), y.floor(), z.floor());
|
||||
let (u, v, w) = (fade(x - fx), fade(y - fy), fade(z - fz));
|
||||
let (ix, iy, iz) = (fx as i64, fy as i64, fz as i64);
|
||||
let g = |dx: i64, dy: i64, dz: i64| unit(mix(&[(ix + dx) as u64, (iy + dy) as u64, (iz + dz) as u64])) * 2.0 - 1.0;
|
||||
let l = |a: f64, b: f64, t: f64| a + (b - a) * t;
|
||||
let x00 = l(g(0, 0, 0), g(1, 0, 0), u);
|
||||
let x10 = l(g(0, 1, 0), g(1, 1, 0), u);
|
||||
let x01 = l(g(0, 0, 1), g(1, 0, 1), u);
|
||||
let x11 = l(g(0, 1, 1), g(1, 1, 1), u);
|
||||
l(l(x00, x10, v), l(x01, x11, v), w)
|
||||
}
|
||||
|
||||
/// Per-dimension wiggle offsets: `Σ amp·mult^k · noise(freq·2^k·t)` over `octaves`.
|
||||
/// With one octave every offset lies in `[-amp, amp]`.
|
||||
pub fn wiggle(seed: u64, dims: usize, freq: f64, amp: f64, octaves: f64, amp_mult: f64, t: f64) -> Vec<f64> {
|
||||
let octaves = if octaves.is_finite() { octaves.clamp(1.0, 16.0) as usize } else { 1 };
|
||||
(0..dims)
|
||||
.map(|d| {
|
||||
let mut a = amp;
|
||||
let mut f = freq;
|
||||
let mut sum = 0.0;
|
||||
for k in 0..octaves {
|
||||
let s = mix(&[seed, d as u64, k as u64]);
|
||||
// Offset each octave's phase so octaves don't share lattice points.
|
||||
sum += a * noise1(s, f * t + unit(s) * 1000.0);
|
||||
a *= amp_mult;
|
||||
f *= 2.0;
|
||||
}
|
||||
sum
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn noise_is_bounded_smooth_and_deterministic() {
|
||||
let mut prev = noise1(7, 0.0);
|
||||
for i in 1..10_000 {
|
||||
let x = i as f64 * 0.001;
|
||||
let v = noise1(7, x);
|
||||
assert!((-1.0..=1.0).contains(&v));
|
||||
assert!((v - prev).abs() < 0.01, "jump at {x}");
|
||||
prev = v;
|
||||
}
|
||||
assert_eq!(noise1(3, 1.25), noise1(3, 1.25));
|
||||
assert_ne!(noise1(3, 1.25), noise1(4, 1.25));
|
||||
assert!((-1.0..=1.0).contains(&noise3(0.3, 4.2, -7.5)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wiggle_bounds() {
|
||||
for i in 0..2000 {
|
||||
let w = wiggle(99, 2, 3.0, 50.0, 1.0, 0.5, i as f64 / 30.0);
|
||||
assert!(w.iter().all(|v| v.abs() <= 50.0));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user