alice-sdf 5.0.0

ALICE-SDF: 3D/Spatial Data Specialist — Store laws, not polygons. GLSL/WGSL/HLSL transpile, SVO, GPU marching cubes, texture-fit, procedural CSG. Optional bridges to alice-physics / alice-codec / libasp / alice-cache behind features.
Documentation
//! Noise modifiers for SDFs (Deep Fried Edition)
//!
//! # Deep Fried Optimizations
//! - **Forced Inlining**: All helper functions (`fade`, `lerp`, `grad`, `hash`) are
//!   forced inline to allow the compiler to flatten the noise calculation loop.
//! - **Branchless Grad**: Optimized gradient selection logic.
//!
//! Author: Moroya Sakamoto

use glam::Vec3;

/// Apply Perlin noise displacement to a distance value
#[inline(always)]
pub fn modifier_noise_perlin(
    distance: f32,
    point: Vec3,
    amplitude: f32,
    frequency: f32,
    seed: u32,
) -> f32 {
    let noise = perlin_noise_3d(
        point.x * frequency,
        point.y * frequency,
        point.z * frequency,
        seed,
    );
    noise * amplitude + distance
}

/// Apply simplex-like noise displacement
#[inline(always)]
pub fn modifier_noise_simplex(
    distance: f32,
    point: Vec3,
    amplitude: f32,
    frequency: f32,
    seed: u32,
) -> f32 {
    // For now, mapping to perlin or similar fast noise
    // In deep fried context, we prefer consistent pipelines
    modifier_noise_perlin(distance, point, amplitude, frequency, seed)
}

/// 3D Perlin noise implementation (Deep Fried)
#[inline(always)]
pub fn perlin_noise_3d(x: f32, y: f32, z: f32, seed: u32) -> f32 {
    // `floor() as i32` saturates at `i32::MIN` / `i32::MAX` for |x| >= 2^31 (and maps NaN to 0), so the
    // neighbouring lattice index `+ 1` below must wrap: a plain `+` overflows and panics in debug
    // builds for a far point, while release builds already wrapped. `wrapping_add` makes the two
    // agree and gives every finite coordinate a well-defined value.
    let xi = x.floor() as i32;
    let yi = y.floor() as i32;
    let zi = z.floor() as i32;
    let (xi1, yi1, zi1) = (xi.wrapping_add(1), yi.wrapping_add(1), zi.wrapping_add(1));

    let xf = x - x.floor();
    let yf = y - y.floor();
    let zf = z - z.floor();

    let u = fade(xf);
    let v = fade(yf);
    let w = fade(zf);

    // Unrolled and Inlined Gradient Fetch
    let aaa = grad3d(hash3d(xi, yi, zi, seed), xf, yf, zf);
    let aba = grad3d(hash3d(xi, yi1, zi, seed), xf, yf - 1.0, zf);
    let aab = grad3d(hash3d(xi, yi, zi1, seed), xf, yf, zf - 1.0);
    let abb = grad3d(hash3d(xi, yi1, zi1, seed), xf, yf - 1.0, zf - 1.0);
    let baa = grad3d(hash3d(xi1, yi, zi, seed), xf - 1.0, yf, zf);
    let bba = grad3d(hash3d(xi1, yi1, zi, seed), xf - 1.0, yf - 1.0, zf);
    let bab = grad3d(hash3d(xi1, yi, zi1, seed), xf - 1.0, yf, zf - 1.0);
    let bbb = grad3d(hash3d(xi1, yi1, zi1, seed), xf - 1.0, yf - 1.0, zf - 1.0);

    lerp(
        lerp(lerp(aaa, baa, u), lerp(aba, bba, u), v),
        lerp(lerp(aab, bab, u), lerp(abb, bbb, u), v),
        w,
    )
}

/// Fractal Brownian Motion (fBM) noise
#[inline(always)]
pub fn fbm_noise_3d(
    x: f32,
    y: f32,
    z: f32,
    seed: u32,
    octaves: u32,
    lacunarity: f32,
    persistence: f32,
) -> f32 {
    let mut value = 0.0;
    let mut amplitude = 1.0;
    let mut frequency = 1.0;
    let mut max_value = 0.0;

    for i in 0..octaves {
        // `wrapping_add`: the release-build value of `seed + i`; a plain `+` panics in debug
        // builds once `seed` is within `octaves` of `u32::MAX`
        let octave_seed = seed.wrapping_add(i);
        value +=
            amplitude * perlin_noise_3d(x * frequency, y * frequency, z * frequency, octave_seed);
        max_value += amplitude;
        amplitude *= persistence;
        frequency *= lacunarity;
    }

    // Division Exorcism: reciprocal multiplication instead of division
    value * (1.0 / max_value)
}

// Helper functions - all forced inline for maximum optimization

#[inline(always)]
fn fade(t: f32) -> f32 {
    t * t * t * (t * (t * 6.0 + -15.0) + 10.0)
}

#[inline(always)]
fn lerp(a: f32, b: f32, t: f32) -> f32 {
    t * (b - a) + a
}

#[inline(always)]
const fn hash3d(x: i32, y: i32, z: i32, seed: u32) -> u32 {
    let mut h = seed;
    h ^= x as u32;
    h = h.wrapping_mul(0x85EBCA6B);
    h ^= y as u32;
    h = h.wrapping_mul(0xC2B2AE35);
    h ^= z as u32;
    h = h.wrapping_mul(0x27D4EB2D);
    h ^= h >> 16;
    h
}

/// Branchless gradient selection via lookup table.
///
/// The 16 Perlin gradient vectors are encoded as (u_src, v_src, u_sign, v_sign):
/// - u_src/v_src: 0=x, 1=y, 2=z
/// - u_sign/v_sign: 0=positive, 1=negative
///
/// This eliminates all conditional branches from the inner loop.
#[inline(always)]
fn grad3d(hash: u32, x: f32, y: f32, z: f32) -> f32 {
    // Packed LUT: each entry is (u_src << 4 | v_src << 2 | u_sign << 1 | v_sign)
    // Derived from Perlin's original gradient table
    const LUT: [u8; 16] = [
        0b_00_01_00, // h=0:  +x +y
        0b_00_01_01, // h=1:  +x -y
        0b_00_01_10, // h=2:  -x +y
        0b_00_01_11, // h=3:  -x -y
        0b_00_10_00, // h=4:  +x +z
        0b_00_10_01, // h=5:  +x -z
        0b_00_10_10, // h=6:  -x +z
        0b_00_10_11, // h=7:  -x -z
        0b_01_10_00, // h=8:  +y +z
        0b_01_10_01, // h=9:  +y -z
        0b_01_10_10, // h=10: -y +z
        0b_01_10_11, // h=11: -y -z
        0b_01_00_00, // h=12: +y +x
        0b_00_10_10, // h=13: -x +z  (same as h=6)
        0b_01_00_10, // h=14: -y +x
        0b_00_10_01, // h=15: +x -z  (same as h=5)
    ];

    let entry = LUT[(hash & 15) as usize];
    let coords = [x, y, z];

    let u_src = ((entry >> 4) & 3) as usize;
    let v_src = ((entry >> 2) & 3) as usize;
    let u_neg = (entry >> 1) & 1;
    let v_neg = entry & 1;

    // Branchless sign application: val * (1 - 2 * sign_bit)
    let u = coords[u_src] * (2.0f32 * -(u_neg as f32) + 1.0);
    let v = coords[v_src] * (2.0f32 * -(v_neg as f32) + 1.0);
    u + v
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_perlin_range() {
        let mut min = f32::MAX;
        let mut max = f32::MIN;

        for i in 0..1000 {
            let x = (i as f32) * 0.1;
            let y = (i as f32) * 0.13;
            let z = (i as f32) * 0.17;
            let n = perlin_noise_3d(x, y, z, 42);
            min = min.min(n);
            max = max.max(n);
        }

        assert!(min >= -1.5);
        assert!(max <= 1.5);
    }

    #[test]
    fn test_perlin_deterministic() {
        let n1 = perlin_noise_3d(1.5, 2.5, 3.5, 42);
        let n2 = perlin_noise_3d(1.5, 2.5, 3.5, 42);
        assert!((n1 - n2).abs() < 0.0001);
    }

    #[test]
    fn test_modifier_noise() {
        let original = 1.0;
        let noisy = modifier_noise_perlin(original, Vec3::new(1.0, 2.0, 3.0), 0.1, 1.0, 42);
        assert!((noisy - original).abs() <= 0.15);
    }

    #[test]
    fn test_fbm() {
        let n = fbm_noise_3d(1.5, 2.5, 3.5, 42, 4, 2.0, 0.5);
        assert!((-1.0..=1.0).contains(&n));
    }
}