use std::f64::consts::TAU;
use noise::{Fbm, MultiFractal, Perlin};
use crate::{
generator::{TextureError, TextureGenerator, TextureMap, Workspace, validate_dimensions},
noise::{ToroidalNoise, normalize, sample_grid_into},
surface::{SurfaceCell, SurfaceSample, generate_surface, lerp},
};
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct SandConfig {
pub seed: u32,
pub ripple_count: f64,
pub ripple_warp: f64,
pub grain_density: f64,
pub grain_scale: f64,
pub color_crest: [f32; 3],
pub color_trough: [f32; 3],
pub normal_strength: f32,
}
impl Default for SandConfig {
fn default() -> Self {
Self {
seed: 91,
ripple_count: 10.0,
ripple_warp: 0.6,
grain_density: 0.12,
grain_scale: 24.0,
color_crest: [0.86, 0.74, 0.52],
color_trough: [0.62, 0.50, 0.34],
normal_strength: 2.5,
}
}
}
pub struct SandGenerator {
config: SandConfig,
warp_noise: ToroidalNoise<Fbm<Perlin>>,
grain_noise: ToroidalNoise<Fbm<Perlin>>,
}
impl SandGenerator {
pub fn new(config: SandConfig) -> Self {
let fbm_warp: Fbm<Perlin> = Fbm::new(config.seed).set_octaves(3);
let warp_noise = ToroidalNoise::new(fbm_warp, 2.0);
let fbm_grain: Fbm<Perlin> = Fbm::new(config.seed.wrapping_add(70)).set_octaves(2);
let grain_noise = ToroidalNoise::new(fbm_grain, config.grain_scale.clamp(8.0, 48.0));
Self {
config,
warp_noise,
grain_noise,
}
}
fn generate_inner(
&self,
width: u32,
height: u32,
mut ws: Option<&mut Workspace>,
) -> Result<TextureMap, TextureError> {
validate_dimensions(width, height)?;
let c = &self.config;
let mut warp_grid = ws.as_deref_mut().map_or_else(Vec::new, |w| w.take_grid());
sample_grid_into(&self.warp_noise, width, height, &mut warp_grid);
let mut grain_grid = ws.as_deref_mut().map_or_else(Vec::new, |w| w.take_grid());
sample_grid_into(&self.grain_noise, width, height, &mut grain_grid);
let cell = SandCell {
config: c,
warp_grid: &warp_grid,
grain_grid: &grain_grid,
ripples: c.ripple_count.round().clamp(1.0, 64.0),
fleck_threshold: 1.0 - c.grain_density.clamp(0.0, 0.5),
width: width as usize,
};
let result = generate_surface(width, height, c.normal_strength, ws.as_deref_mut(), &cell);
if let Some(ws) = ws {
ws.return_grid(warp_grid);
ws.return_grid(grain_grid);
}
result
}
}
impl TextureGenerator for SandGenerator {
fn generate(&self, width: u32, height: u32) -> Result<TextureMap, TextureError> {
self.generate_inner(width, height, None)
}
fn generate_with_workspace(
&self,
width: u32,
height: u32,
workspace: &mut Workspace,
) -> Result<TextureMap, TextureError> {
self.generate_inner(width, height, Some(workspace))
}
}
struct SandCell<'a> {
config: &'a SandConfig,
warp_grid: &'a [f64],
grain_grid: &'a [f64],
ripples: f64,
fleck_threshold: f64,
width: usize,
}
impl SurfaceCell for SandCell<'_> {
fn sample(&self, x: u32, y: u32, _u: f64, v: f64) -> SurfaceSample {
let c = self.config;
let idx = y as usize * self.width + x as usize;
let warp = self.warp_grid[idx]; let phase = v * self.ripples * TAU + warp * c.ripple_warp.clamp(0.0, 1.5) * TAU * 0.5;
let ripple = (phase.sin() * 0.5 + 0.5).powf(1.2);
let grain = (normalize(self.grain_grid[idx]) - 0.5) * 2.2 + 0.5;
let fleck = grain > self.fleck_threshold;
let micro = (grain - 0.5).clamp(-0.5, 0.5) * 0.08;
let height =
(ripple * 0.75 + 0.10 + micro + if fleck { 0.06 } else { 0.0 }).clamp(0.0, 1.0);
let t = ripple as f32;
let bright = if fleck { 1.25 } else { 1.0 };
let color = [
(lerp(c.color_trough[0], c.color_crest[0], t) * bright).clamp(0.0, 1.0),
(lerp(c.color_trough[1], c.color_crest[1], t) * bright).clamp(0.0, 1.0),
(lerp(c.color_trough[2], c.color_crest[2], t) * bright).clamp(0.0, 1.0),
];
let rough = (0.93 - t * 0.05 - if fleck { 0.25 } else { 0.0 }).clamp(0.0, 1.0);
SurfaceSample::matte(height, color, rough)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn generator_produces_correct_buffer_sizes() {
let map = SandGenerator::new(SandConfig::default())
.generate(64, 64)
.expect("generate failed");
assert_eq!(map.albedo.len(), 64 * 64 * 4);
assert_eq!(map.normal.len(), 64 * 64 * 4);
}
#[test]
fn ripples_produce_relief_and_flecks_appear() {
let map = SandGenerator::new(SandConfig::default())
.generate(128, 128)
.expect("generate failed");
let flat = map.normal.chunks(4).all(|px| px[0] == 128 && px[1] == 128);
assert!(!flat, "ripples should produce non-flat normals");
let smooth = map.roughness.chunks(4).any(|px| px[1] < 180);
assert!(smooth, "flecks should lower roughness locally");
}
#[test]
fn deterministic_for_same_seed() {
let a = SandGenerator::new(SandConfig::default())
.generate(32, 32)
.expect("generate failed");
let b = SandGenerator::new(SandConfig::default())
.generate(32, 32)
.expect("generate failed");
assert_eq!(a.albedo, b.albedo);
}
}