use crate::node::{GkNode, NodeMeta, Port, Slot, Value};
struct PermTable {
perm: [u8; 512],
}
impl PermTable {
fn new(seed: u64) -> Self {
use xxhash_rust::xxh3::xxh3_64;
let mut p: Vec<u8> = (0..=255).collect();
let mut s = seed;
for i in (1..256).rev() {
s = xxh3_64(&s.to_le_bytes());
let j = (s as usize) % (i + 1);
p.swap(i, j);
}
let mut perm = [0u8; 512];
for i in 0..512 {
perm[i] = p[i & 255];
}
Self { perm }
}
#[inline]
fn hash(&self, i: i32) -> u8 {
self.perm[(i & 255) as usize]
}
}
#[inline]
fn fade(t: f64) -> f64 {
t * t * t * (t * (t * 6.0 - 15.0) + 10.0)
}
#[inline]
fn lerp(t: f64, a: f64, b: f64) -> f64 {
a + t * (b - a)
}
#[inline]
fn grad1d(hash: u8, x: f64) -> f64 {
if hash & 1 == 0 { x } else { -x }
}
#[inline]
fn grad2d(hash: u8, x: f64, y: f64) -> f64 {
match hash & 3 {
0 => x + y,
1 => -x + y,
2 => x - y,
_ => -x - y,
}
}
fn perlin_1d(perm: &PermTable, x: f64) -> f64 {
let xi = x.floor() as i32;
let xf = x - x.floor();
let u = fade(xf);
let a = perm.hash(xi);
let b = perm.hash(xi + 1);
lerp(u, grad1d(a, xf), grad1d(b, xf - 1.0))
}
fn perlin_2d(perm: &PermTable, x: f64, y: f64) -> f64 {
let xi = x.floor() as i32;
let yi = y.floor() as i32;
let xf = x - x.floor();
let yf = y - y.floor();
let u = fade(xf);
let v = fade(yf);
let aa = perm.hash(perm.hash(xi) as i32 + yi);
let ab = perm.hash(perm.hash(xi) as i32 + yi + 1);
let ba = perm.hash(perm.hash(xi + 1) as i32 + yi);
let bb = perm.hash(perm.hash(xi + 1) as i32 + yi + 1);
lerp(v,
lerp(u, grad2d(aa, xf, yf), grad2d(ba, xf - 1.0, yf)),
lerp(u, grad2d(ab, xf, yf - 1.0), grad2d(bb, xf - 1.0, yf - 1.0)),
)
}
const F2: f64 = 0.3660254037844386; const G2: f64 = 0.21132486540518713;
fn simplex_2d(perm: &PermTable, x: f64, y: f64) -> f64 {
let s = (x + y) * F2;
let i = (x + s).floor() as i32;
let j = (y + s).floor() as i32;
let t = (i + j) as f64 * G2;
let x0 = x - (i as f64 - t);
let y0 = y - (j as f64 - t);
let (i1, j1) = if x0 > y0 { (1, 0) } else { (0, 1) };
let x1 = x0 - i1 as f64 + G2;
let y1 = y0 - j1 as f64 + G2;
let x2 = x0 - 1.0 + 2.0 * G2;
let y2 = y0 - 1.0 + 2.0 * G2;
let gi0 = perm.hash(i + perm.hash(j) as i32);
let gi1 = perm.hash(i + i1 + perm.hash(j + j1) as i32);
let gi2 = perm.hash(i + 1 + perm.hash(j + 1) as i32);
let mut n0 = 0.0;
let t0 = 0.5 - x0 * x0 - y0 * y0;
if t0 > 0.0 {
let t0 = t0 * t0;
n0 = t0 * t0 * grad2d(gi0, x0, y0);
}
let mut n1 = 0.0;
let t1 = 0.5 - x1 * x1 - y1 * y1;
if t1 > 0.0 {
let t1 = t1 * t1;
n1 = t1 * t1 * grad2d(gi1, x1, y1);
}
let mut n2 = 0.0;
let t2 = 0.5 - x2 * x2 - y2 * y2;
if t2 > 0.0 {
let t2 = t2 * t2;
n2 = t2 * t2 * grad2d(gi2, x2, y2);
}
70.0 * (n0 + n1 + n2)
}
pub struct Perlin1D {
meta: NodeMeta,
perm: PermTable,
frequency: f64,
}
impl Perlin1D {
pub fn new(seed: u64, frequency: f64) -> Self {
Self {
meta: NodeMeta {
name: "perlin_1d".into(),
outs: vec![Port::f64("output")],
ins: vec![Slot::Wire(Port::u64("input"))],
},
perm: PermTable::new(seed),
frequency,
}
}
}
impl GkNode for Perlin1D {
fn meta(&self) -> &NodeMeta { &self.meta }
fn eval(&self, inputs: &[Value], outputs: &mut [Value]) {
let x = inputs[0].as_u64() as f64 * self.frequency;
outputs[0] = Value::F64(perlin_1d(&self.perm, x));
}
}
pub struct Perlin2D {
meta: NodeMeta,
perm: PermTable,
frequency: f64,
}
impl Perlin2D {
pub fn new(seed: u64, frequency: f64) -> Self {
Self {
meta: NodeMeta {
name: "perlin_2d".into(),
outs: vec![Port::f64("output")],
ins: vec![Slot::Wire(Port::u64("x")), Slot::Wire(Port::u64("y"))],
},
perm: PermTable::new(seed),
frequency,
}
}
}
impl GkNode for Perlin2D {
fn meta(&self) -> &NodeMeta { &self.meta }
fn eval(&self, inputs: &[Value], outputs: &mut [Value]) {
let x = inputs[0].as_u64() as f64 * self.frequency;
let y = inputs[1].as_u64() as f64 * self.frequency;
outputs[0] = Value::F64(perlin_2d(&self.perm, x, y));
}
}
pub struct SimplexNoise2D {
meta: NodeMeta,
perm: PermTable,
frequency: f64,
}
impl SimplexNoise2D {
pub fn new(seed: u64, frequency: f64) -> Self {
Self {
meta: NodeMeta {
name: "simplex_2d".into(),
outs: vec![Port::f64("output")],
ins: vec![Slot::Wire(Port::u64("x")), Slot::Wire(Port::u64("y"))],
},
perm: PermTable::new(seed),
frequency,
}
}
}
impl GkNode for SimplexNoise2D {
fn meta(&self) -> &NodeMeta { &self.meta }
fn eval(&self, inputs: &[Value], outputs: &mut [Value]) {
let x = inputs[0].as_u64() as f64 * self.frequency;
let y = inputs[1].as_u64() as f64 * self.frequency;
outputs[0] = Value::F64(simplex_2d(&self.perm, x, y));
}
}
pub struct FractalNoise1D {
meta: NodeMeta,
perm: PermTable,
frequency: f64,
octaves: u32,
lacunarity: f64,
persistence: f64,
}
impl FractalNoise1D {
pub fn new(seed: u64, frequency: f64, octaves: u32) -> Self {
Self::with_params(seed, frequency, octaves, 2.0, 0.5)
}
pub fn with_params(
seed: u64, frequency: f64, octaves: u32,
lacunarity: f64, persistence: f64,
) -> Self {
Self {
meta: NodeMeta {
name: "fractal_noise_1d".into(),
outs: vec![Port::f64("output")],
ins: vec![Slot::Wire(Port::u64("input"))],
},
perm: PermTable::new(seed),
frequency,
octaves,
lacunarity,
persistence,
}
}
}
impl GkNode for FractalNoise1D {
fn meta(&self) -> &NodeMeta { &self.meta }
fn eval(&self, inputs: &[Value], outputs: &mut [Value]) {
let base_x = inputs[0].as_u64() as f64;
let mut total = 0.0;
let mut freq = self.frequency;
let mut amp = 1.0;
let mut max_amp = 0.0;
for _ in 0..self.octaves {
total += perlin_1d(&self.perm, base_x * freq) * amp;
max_amp += amp;
freq *= self.lacunarity;
amp *= self.persistence;
}
outputs[0] = Value::F64(total / max_amp);
}
}
pub struct FractalNoise2D {
meta: NodeMeta,
perm: PermTable,
frequency: f64,
octaves: u32,
lacunarity: f64,
persistence: f64,
}
impl FractalNoise2D {
pub fn new(seed: u64, frequency: f64, octaves: u32) -> Self {
Self::with_params(seed, frequency, octaves, 2.0, 0.5)
}
pub fn with_params(
seed: u64, frequency: f64, octaves: u32,
lacunarity: f64, persistence: f64,
) -> Self {
Self {
meta: NodeMeta {
name: "fractal_noise_2d".into(),
outs: vec![Port::f64("output")],
ins: vec![Slot::Wire(Port::u64("x")), Slot::Wire(Port::u64("y"))],
},
perm: PermTable::new(seed),
frequency,
octaves,
lacunarity,
persistence,
}
}
}
impl GkNode for FractalNoise2D {
fn meta(&self) -> &NodeMeta { &self.meta }
fn eval(&self, inputs: &[Value], outputs: &mut [Value]) {
let base_x = inputs[0].as_u64() as f64;
let base_y = inputs[1].as_u64() as f64;
let mut total = 0.0;
let mut freq = self.frequency;
let mut amp = 1.0;
let mut max_amp = 0.0;
for _ in 0..self.octaves {
total += perlin_2d(&self.perm, base_x * freq, base_y * freq) * amp;
max_amp += amp;
freq *= self.lacunarity;
amp *= self.persistence;
}
outputs[0] = Value::F64(total / max_amp);
}
}
use crate::dsl::registry::{Arity, FuncCategory, FuncSig, ParamSpec};
use crate::node::SlotType;
pub fn signatures() -> &'static [FuncSig] {
use FuncCategory as C;
&[
FuncSig {
name: "perlin_1d", category: C::Noise,
outputs: 1, description: "1D Perlin noise",
identity: None, variadic_ctor: None,
params: &[
ParamSpec { name: "input", slot_type: SlotType::Wire, required: true, example: "cycle", constraint: None },
ParamSpec { name: "seed", slot_type: SlotType::ConstU64, required: true, example: "42", constraint: None },
ParamSpec { name: "frequency", slot_type: SlotType::ConstF64, required: true, example: "1.0", constraint: None },
],
arity: Arity::Fixed,
commutativity: crate::node::Commutativity::Positional,
help: "1D Perlin noise: coherent pseudo-random f64 in [-1, 1].\nThe u64 input is scaled to the float domain by frequency.\nNearby inputs produce smoothly varying outputs (spatial correlation).\nParameters:\n input — u64 wire input\n seed — permutation table seed (u64)\n frequency — spatial frequency (f64; higher = more detail)\nExample: perlin_1d(cycle, 42, 0.01) // slow-varying noise",
default_resolver: None,
output_type: crate::dsl::registry::OutputType::Fixed,
},
FuncSig {
name: "perlin_2d", category: C::Noise,
outputs: 1, description: "2D Perlin noise",
identity: None, variadic_ctor: None,
params: &[
ParamSpec { name: "x", slot_type: SlotType::Wire, required: true, example: "cycle", constraint: None },
ParamSpec { name: "y", slot_type: SlotType::Wire, required: true, example: "cycle", constraint: None },
ParamSpec { name: "seed", slot_type: SlotType::ConstU64, required: true, example: "42", constraint: None },
ParamSpec { name: "frequency", slot_type: SlotType::ConstF64, required: true, example: "1.0", constraint: None },
],
arity: Arity::Fixed,
commutativity: crate::node::Commutativity::Positional,
help: "2D Perlin noise: coherent pseudo-random f64 in [-1, 1].\nTwo u64 coordinate inputs are scaled by frequency.\nProduces spatially correlated values for terrain, textures, etc.\nParameters:\n x — u64 x-coordinate wire input\n y — u64 y-coordinate wire input\n seed — permutation table seed (u64)\n frequency — spatial frequency (f64)\nExample: perlin_2d(row, col, 42, 0.05)",
default_resolver: None,
output_type: crate::dsl::registry::OutputType::Fixed,
},
FuncSig {
name: "simplex_2d", category: C::Noise,
outputs: 1, description: "2D simplex noise",
identity: None, variadic_ctor: None,
params: &[
ParamSpec { name: "x", slot_type: SlotType::Wire, required: true, example: "cycle", constraint: None },
ParamSpec { name: "y", slot_type: SlotType::Wire, required: true, example: "cycle", constraint: None },
ParamSpec { name: "seed", slot_type: SlotType::ConstU64, required: true, example: "42", constraint: None },
ParamSpec { name: "frequency", slot_type: SlotType::ConstF64, required: true, example: "1.0", constraint: None },
],
arity: Arity::Fixed,
commutativity: crate::node::Commutativity::Positional,
help: "2D simplex noise: faster than Perlin for 2D+ with fewer directional artifacts.\nOutput is f64 in [-1, 1]. Uses a simplex grid instead of a square grid.\nParameters:\n x — u64 x-coordinate wire input\n y — u64 y-coordinate wire input\n seed — permutation table seed (u64)\n frequency — spatial frequency (f64)\nExample: simplex_2d(row, col, 99, 0.1)",
default_resolver: None,
output_type: crate::dsl::registry::OutputType::Fixed,
},
FuncSig {
name: "fractal_noise_1d", category: C::Noise,
outputs: 1, description: "1D fractal Brownian motion",
identity: None, variadic_ctor: None,
params: &[
ParamSpec { name: "input", slot_type: SlotType::Wire, required: true, example: "cycle", constraint: None },
ParamSpec { name: "seed", slot_type: SlotType::ConstU64, required: true, example: "42", constraint: None },
ParamSpec { name: "frequency", slot_type: SlotType::ConstF64, required: true, example: "1.0", constraint: None },
ParamSpec { name: "octaves", slot_type: SlotType::ConstU64, required: false, example: "4", constraint: None },
],
arity: Arity::Fixed,
commutativity: crate::node::Commutativity::Positional,
help: "1D fractal Brownian motion: layered Perlin noise with decreasing\namplitude at each octave. Produces rich, natural-looking signals.\nOutput is f64, roughly in [-1, 1].\nParameters:\n input — u64 wire input\n seed — permutation table seed (u64)\n frequency — base spatial frequency (f64)\n octaves — number of noise layers (u64, default 4)\nExample: fractal_noise_1d(cycle, 42, 0.02, 4)",
default_resolver: None,
output_type: crate::dsl::registry::OutputType::Fixed,
},
FuncSig {
name: "fractal_noise_2d", category: C::Noise,
outputs: 1, description: "2D fractal Brownian motion",
identity: None, variadic_ctor: None,
params: &[
ParamSpec { name: "x", slot_type: SlotType::Wire, required: true, example: "cycle", constraint: None },
ParamSpec { name: "y", slot_type: SlotType::Wire, required: true, example: "cycle", constraint: None },
ParamSpec { name: "seed", slot_type: SlotType::ConstU64, required: true, example: "42", constraint: None },
ParamSpec { name: "frequency", slot_type: SlotType::ConstF64, required: true, example: "1.0", constraint: None },
ParamSpec { name: "octaves", slot_type: SlotType::ConstU64, required: false, example: "4", constraint: None },
],
arity: Arity::Fixed,
commutativity: crate::node::Commutativity::Positional,
help: "2D fractal Brownian motion: layered Perlin noise in 2D.\nProduces terrain-like spatial variation.\nParameters:\n x, y — u64 coordinate wire inputs\n seed — permutation table seed (u64)\n frequency — base spatial frequency (f64)\n octaves — number of noise layers (u64, default 4)\nExample: fractal_noise_2d(row, col, 42, 0.05, 4)",
default_resolver: None,
output_type: crate::dsl::registry::OutputType::Fixed,
},
]
}
pub(crate) fn build_node(name: &str, _wires: &[crate::assembly::WireRef], _wire_types: &[crate::node::PortType], consts: &[crate::dsl::factory::ConstArg]) -> Option<Result<Box<dyn crate::node::GkNode>, String>> {
match name {
"perlin_1d" => Some(Ok(Box::new(Perlin1D::new(
consts.first().map(|c| c.as_u64()).unwrap_or(0),
consts.get(1).map(|c| c.as_f64()).unwrap_or(0.01),
)))),
"perlin_2d" => Some(Ok(Box::new(Perlin2D::new(
consts.first().map(|c| c.as_u64()).unwrap_or(0),
consts.get(1).map(|c| c.as_f64()).unwrap_or(0.01),
)))),
"simplex_2d" => Some(Ok(Box::new(SimplexNoise2D::new(
consts.first().map(|c| c.as_u64()).unwrap_or(0),
consts.get(1).map(|c| c.as_f64()).unwrap_or(0.01),
)))),
"fractal_noise_1d" => Some(Ok(Box::new(FractalNoise1D::new(
consts.first().map(|c| c.as_u64()).unwrap_or(0),
consts.get(1).map(|c| c.as_f64()).unwrap_or(0.02),
consts.get(2).map(|c| c.as_u64() as u32).unwrap_or(4),
)))),
"fractal_noise_2d" => Some(Ok(Box::new(FractalNoise2D::new(
consts.first().map(|c| c.as_u64()).unwrap_or(0),
consts.get(1).map(|c| c.as_f64()).unwrap_or(0.02),
consts.get(2).map(|c| c.as_u64() as u32).unwrap_or(4),
)))),
_ => None,
}
}
crate::register_nodes!(signatures, build_node);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn perlin_1d_bounded() {
let node = Perlin1D::new(42, 0.01);
let mut out = [Value::None];
for i in 0..1000u64 {
node.eval(&[Value::U64(i)], &mut out);
let v = out[0].as_f64();
assert!(v >= -1.0 && v <= 1.0, "out of range: {v} at i={i}");
}
}
#[test]
fn perlin_1d_smooth() {
let node = Perlin1D::new(42, 0.01);
let mut prev = [Value::None];
let mut curr = [Value::None];
node.eval(&[Value::U64(100)], &mut prev);
let mut large_jumps = 0;
for i in 101..200u64 {
node.eval(&[Value::U64(i)], &mut curr);
let diff = (curr[0].as_f64() - prev[0].as_f64()).abs();
if diff > 0.5 { large_jumps += 1; }
prev[0] = curr[0].clone();
}
assert!(large_jumps < 5, "too many large jumps: {large_jumps}");
}
#[test]
fn perlin_1d_deterministic() {
let node = Perlin1D::new(42, 0.1);
let mut out1 = [Value::None];
let mut out2 = [Value::None];
node.eval(&[Value::U64(123)], &mut out1);
node.eval(&[Value::U64(123)], &mut out2);
assert_eq!(out1[0].as_f64(), out2[0].as_f64());
}
#[test]
fn perlin_1d_different_seeds() {
let a = Perlin1D::new(1, 0.1);
let b = Perlin1D::new(2, 0.1);
let mut out_a = [Value::None];
let mut out_b = [Value::None];
let mut differ = false;
for i in 0..100u64 {
a.eval(&[Value::U64(i)], &mut out_a);
b.eval(&[Value::U64(i)], &mut out_b);
if (out_a[0].as_f64() - out_b[0].as_f64()).abs() > 0.01 {
differ = true;
break;
}
}
assert!(differ, "different seeds should produce different noise");
}
#[test]
fn perlin_2d_bounded() {
let node = Perlin2D::new(42, 0.01);
let mut out = [Value::None];
for x in 0..50u64 {
for y in 0..50u64 {
node.eval(&[Value::U64(x), Value::U64(y)], &mut out);
let v = out[0].as_f64();
assert!(v >= -1.5 && v <= 1.5, "out of range: {v} at ({x},{y})");
}
}
}
#[test]
fn perlin_2d_smooth() {
let node = Perlin2D::new(42, 0.01);
let mut prev = [Value::None];
let mut curr = [Value::None];
node.eval(&[Value::U64(100), Value::U64(100)], &mut prev);
let mut large_jumps = 0;
for i in 101..150u64 {
node.eval(&[Value::U64(i), Value::U64(100)], &mut curr);
let diff = (curr[0].as_f64() - prev[0].as_f64()).abs();
if diff > 0.5 { large_jumps += 1; }
prev[0] = curr[0].clone();
}
assert!(large_jumps < 5, "too many large jumps: {large_jumps}");
}
#[test]
fn simplex_2d_bounded() {
let node = SimplexNoise2D::new(42, 0.01);
let mut out = [Value::None];
for x in 0..50u64 {
for y in 0..50u64 {
node.eval(&[Value::U64(x), Value::U64(y)], &mut out);
let v = out[0].as_f64();
assert!(v >= -1.5 && v <= 1.5, "out of range: {v}");
}
}
}
#[test]
fn fractal_1d_bounded() {
let node = FractalNoise1D::new(42, 0.01, 4);
let mut out = [Value::None];
for i in 0..500u64 {
node.eval(&[Value::U64(i)], &mut out);
let v = out[0].as_f64();
assert!(v >= -1.5 && v <= 1.5, "out of range: {v}");
}
}
#[test]
fn fractal_1d_more_detail_than_single_octave() {
let single = Perlin1D::new(42, 0.01);
let fbm = FractalNoise1D::new(42, 0.01, 4);
let mut s_out = [Value::None];
let mut f_out = [Value::None];
let mut s_changes = 0.0;
let mut f_changes = 0.0;
let mut s_prev = 0.0;
let mut f_prev = 0.0;
for i in 0..500u64 {
single.eval(&[Value::U64(i)], &mut s_out);
fbm.eval(&[Value::U64(i)], &mut f_out);
if i > 0 {
s_changes += (s_out[0].as_f64() - s_prev).abs();
f_changes += (f_out[0].as_f64() - f_prev).abs();
}
s_prev = s_out[0].as_f64();
f_prev = f_out[0].as_f64();
}
assert!(f_changes > s_changes * 0.8,
"FBM should have comparable or more detail: single={s_changes}, fbm={f_changes}");
}
#[test]
fn fractal_2d_bounded() {
let node = FractalNoise2D::new(42, 0.01, 3);
let mut out = [Value::None];
for x in 0..30u64 {
for y in 0..30u64 {
node.eval(&[Value::U64(x), Value::U64(y)], &mut out);
let v = out[0].as_f64();
assert!(v >= -1.5 && v <= 1.5, "out of range: {v}");
}
}
}
}