use image::{GrayImage, Luma}; use rayon::prelude::*; const TAU: f32 = 6.283_185_5; const EROSION_LACUNARITY: f32 = 2.0; const EROSION_GAIN: f32 = 0.5; const EROSION_CELL_SCALE: f32 = 0.7; const EROSION_NORMALIZATION: f32 = 0.5; const EROSION_ROUNDING: [f32; 4] = [0.1, 0.0, 0.1, 2.0]; const EROSION_ONSET: [f32; 4] = [1.25, 1.25, 2.8, 1.5]; const EROSION_ASSUMED_SLOPE: [f32; 2] = [0.7, 1.0]; #[derive(Clone, Debug)] pub struct GenerationParams { pub image_width: usize, pub image_height: usize, pub generation_scale: f32, pub erosion_scale: f32, pub random_seed: u64, pub erosion_strength: f32, pub erosion_gully_weight: f32, pub erosion_detail: f32, pub erosion_octaves: usize, } impl Default for GenerationParams { fn default() -> Self { Self { image_width: 512, image_height: 512, generation_scale: 1.0, erosion_scale: 0.15, random_seed: 0, erosion_strength: 0.05, erosion_gully_weight: 0.5, erosion_detail: 1.5, erosion_octaves: 5, } } } #[derive(Clone)] pub struct GeneratedImages { pub width: usize, pub height: usize, pub heightmap_image: GrayImage, pub bumpmap_image: GrayImage, } #[derive(Copy, Clone, Debug, Default)] struct Vec2 { x: f32, y: f32, } impl Vec2 { fn new(x: f32, y: f32) -> Self { Self { x, y } } fn dot(self, other: Self) -> f32 { self.x * other.x + self.y * other.y } fn length(self) -> f32 { self.dot(self).sqrt() } fn normalize_safe(self) -> Self { let length = self.length(); if length > 1e-10 { Self::new(self.x / length, self.y / length) } else { self } } } impl std::ops::Add for Vec2 { type Output = Self; fn add(self, rhs: Self) -> Self::Output { Self::new(self.x + rhs.x, self.y + rhs.y) } } impl std::ops::AddAssign for Vec2 { fn add_assign(&mut self, rhs: Self) { self.x += rhs.x; self.y += rhs.y; } } impl std::ops::Sub for Vec2 { type Output = Self; fn sub(self, rhs: Self) -> Self::Output { Self::new(self.x - rhs.x, self.y - rhs.y) } } impl std::ops::Mul for Vec2 { type Output = Self; fn mul(self, rhs: f32) -> Self::Output { Self::new(self.x * rhs, self.y * rhs) } } fn mix(a: f32, b: f32, t: f32) -> f32 { a + (b - a) * t } fn mix2(a: Vec2, b: Vec2, t: f32) -> Vec2 { Vec2::new(mix(a.x, b.x, t), mix(a.y, b.y, t)) } fn clamp(x: f32, lo: f32, hi: f32) -> f32 { x.clamp(lo, hi) } fn clamp01(x: f32) -> f32 { clamp(x, 0.0, 1.0) } fn hash22(p: Vec2, random_seed: u64) -> Vec2 { let scrambled_x = splitmix64(random_seed ^ 0x9E37_79B9_7F4A_7C15); let scrambled_y = splitmix64(random_seed ^ 0xBF58_476D_1CE4_E5B9); let seed_x = u64_to_unit_f64(scrambled_x) * 1024.0; let seed_y = u64_to_unit_f64(scrambled_y) * 1024.0; let kx = 0.318_309_9_f64; let ky = 0.367_879_4_f64; let px = ((p.x as f64) + seed_x) * kx + ky; let py = ((p.y as f64) + seed_y) * ky + kx; let val = px * py * (px + py); let f_val = val - val.floor(); let rx = -1.0 + 2.0 * ((16.0 * kx * f_val) - (16.0 * kx * f_val).floor()); let ry = -1.0 + 2.0 * ((16.0 * ky * f_val) - (16.0 * ky * f_val).floor()); Vec2::new(rx as f32, ry as f32) } fn splitmix64(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) } fn u64_to_unit_f64(value: u64) -> f64 { const SCALE: f64 = 1.0 / (u64::MAX as f64); (value as f64) * SCALE } fn noised(p: Vec2, random_seed: u64) -> (f32, Vec2) { let i = Vec2::new(p.x.floor(), p.y.floor()); let f = Vec2::new(p.x - i.x, p.y - i.y); let u = Vec2::new( f.x * f.x * f.x * (f.x * (f.x * 6.0 - 15.0) + 10.0), f.y * f.y * f.y * (f.y * (f.y * 6.0 - 15.0) + 10.0), ); let du = Vec2::new( 30.0 * f.x * f.x * (f.x * (f.x - 2.0) + 1.0), 30.0 * f.y * f.y * (f.y * (f.y - 2.0) + 1.0), ); let ga = hash22(i + Vec2::new(0.0, 0.0), random_seed); let gb = hash22(i + Vec2::new(1.0, 0.0), random_seed); let gc = hash22(i + Vec2::new(0.0, 1.0), random_seed); let gd = hash22(i + Vec2::new(1.0, 1.0), random_seed); let va = ga.dot(f - Vec2::new(0.0, 0.0)); let vb = gb.dot(f - Vec2::new(1.0, 0.0)); let vc = gc.dot(f - Vec2::new(0.0, 1.0)); let vd = gd.dot(f - Vec2::new(1.0, 1.0)); let val = va + u.x * (vb - va) + u.y * (vc - va) + u.x * u.y * (va - vb - vc + vd); let deriv_x = ga.x + u.x * (gb.x - ga.x) + u.y * (gc.x - ga.x) + u.x * u.y * (ga.x - gb.x - gc.x + gd.x) + du.x * (u.y * (va - vb - vc + vd) + (vb - va)); let deriv_y = ga.y + u.x * (gb.y - ga.y) + u.y * (gc.y - ga.y) + u.x * u.y * (ga.y - gb.y - gc.y + gd.y) + du.y * (u.x * (va - vb - vc + vd) + (vc - va)); (val, Vec2::new(deriv_x, deriv_y)) } fn fractal_noise(p: Vec2, freq: f32, octaves: usize, lacunarity: f32, gain: f32, random_seed: u64) -> (f32, Vec2) { let mut val = 0.0; let mut deriv = Vec2::default(); let mut nf = freq; let mut na = 1.0; for _ in 0..octaves { let (v, d) = noised(p * nf, random_seed); val += v * na; deriv += d * (na * nf); na *= gain; nf *= lacunarity; } (val, deriv) } fn phacelle_noise(p: Vec2, norm_dir: Vec2, freq: f32, offset: f32, normalization: f32, random_seed: u64) -> (Vec2, Vec2) { let side_dir = Vec2::new(-norm_dir.y, norm_dir.x) * (freq * TAU); let offset_tau = offset * TAU; let p_int = Vec2::new(p.x.floor(), p.y.floor()); let p_frac = p - p_int; let mut phase_dir = Vec2::default(); let mut weight_sum = 0.0; for i in -1..3 { for j in -1..3 { let grid_offset = Vec2::new(i as f32, j as f32); let grid_point = p_int + grid_offset; let random_offset = hash22(grid_point, random_seed) * 0.5; let vector_from_cell_point = p_frac - grid_offset - random_offset; let sqr_dist = vector_from_cell_point.dot(vector_from_cell_point); let mut weight = (-sqr_dist * 2.0).exp(); weight = weight.max(0.0) - 0.01111; weight = weight.max(0.0); weight_sum += weight; let wave_input = vector_from_cell_point.dot(side_dir) + offset_tau; phase_dir.x += wave_input.cos() * weight; phase_dir.y += wave_input.sin() * weight; } } if weight_sum < 1e-6 { return (Vec2::default(), side_dir); } let interpolated = phase_dir * (1.0 / weight_sum); let magnitude = interpolated.dot(interpolated).sqrt().max(1.0 - normalization); (interpolated * (1.0 / magnitude), side_dir) } fn pow_inv(t: f32, power: f32) -> f32 { 1.0 - (1.0 - clamp01(t)).powf(power) } fn ease_out(t: f32) -> f32 { let v = 1.0 - clamp01(t); 1.0 - v * v } fn smooth_start(t: f32, smoothing: f32) -> f32 { if t >= smoothing { t - 0.5 * smoothing } else { 0.5 * t * t / smoothing } } pub fn generate(params: &GenerationParams) -> GeneratedImages { let width = params.image_width; let height = params.image_height; let scale = params.erosion_scale; let strength_base = params.erosion_strength * scale; let mut heightmap = vec![0.0_f32; width * height]; heightmap .par_chunks_mut(width) .enumerate() .for_each(|(y, row)| { for (x, value) in row.iter_mut().enumerate() { let p = Vec2::new( x as f32 / width as f32, y as f32 / height as f32, ) * params.generation_scale; let height_freq = 3.0; let height_amp = 0.125; let (val, deriv) = fractal_noise(p, height_freq, 3, 2.0, 0.1, params.random_seed); let n_height = val * height_amp + 0.5; let curr_slope = deriv * height_amp; let mut fade_target = clamp((n_height - 0.5) / (height_amp * 0.6), -1.0, 1.0); let mut freq = 1.0 / (scale * EROSION_CELL_SCALE); let slope_length = curr_slope.length().max(1e-10); let rounding_for_input = mix( EROSION_ROUNDING[1], EROSION_ROUNDING[0], clamp01(fade_target + 0.5), ) * EROSION_ROUNDING[2]; let mut combi_mask = ease_out(smooth_start( slope_length * EROSION_ONSET[0], rounding_for_input * EROSION_ONSET[0], )); let mut gully_slope = mix2( curr_slope, curr_slope * (EROSION_ASSUMED_SLOPE[0] / slope_length), EROSION_ASSUMED_SLOPE[1], ); let mut strength = strength_base; let mut rounding_mult = 1.0; let mut total_h_delta = 0.0; let mut total_strength = 0.0; for _ in 0..params.erosion_octaves { let (phacelle_vec, side_dir) = phacelle_noise( p * freq, gully_slope.normalize_safe(), EROSION_CELL_SCALE, 0.25, EROSION_NORMALIZATION, params.random_seed, ); let p_deriv_dir = side_dir * -freq; let sloping = phacelle_vec.y.abs(); gully_slope += p_deriv_dir * phacelle_vec.y.signum() * strength * params.erosion_gully_weight; let gullies_h = phacelle_vec.x; let faded_gullies_h = mix( fade_target, gullies_h * params.erosion_gully_weight, combi_mask, ); total_h_delta += faded_gullies_h * strength; total_strength += strength; fade_target = faded_gullies_h; let rounding_for_octave = mix( EROSION_ROUNDING[1], EROSION_ROUNDING[0], clamp01(phacelle_vec.x + 0.5), ) * rounding_mult; let new_mask = ease_out(smooth_start( sloping * EROSION_ONSET[1], rounding_for_octave * EROSION_ONSET[1], )); combi_mask = pow_inv(combi_mask, params.erosion_detail) * new_mask; strength *= EROSION_GAIN; freq *= EROSION_LACUNARITY; rounding_mult *= EROSION_ROUNDING[3]; } let terrain_height_offset = [-0.65, 0.0]; let final_fade_target = clamp((n_height - 0.5) / (height_amp * 0.6), -1.0, 1.0); let offset = mix( terrain_height_offset[0], -final_fade_target, terrain_height_offset[1], ) * total_strength; *value = n_height + total_h_delta + offset; } }); let heightmap_normalized = normalize_values(&heightmap); let bumpmap = apply_shading(&heightmap, width, height, 0.5); let heightmap_image = grayscale_image_from_values(&heightmap_normalized, width, height); let bumpmap_image = grayscale_image_from_values(&bumpmap, width, height); GeneratedImages { width, height, heightmap_image, bumpmap_image, } } fn normalize_values(values: &[f32]) -> Vec { let mut min_val = f32::INFINITY; let mut max_val = f32::NEG_INFINITY; for value in values { min_val = min_val.min(*value); max_val = max_val.max(*value); } if (max_val - min_val).abs() < 1e-10 { return vec![0.0; values.len()]; } values .iter() .map(|value| (value - min_val) / (max_val - min_val)) .collect() } fn grayscale_image_from_values(values: &[f32], width: usize, height: usize) -> GrayImage { let mut image = GrayImage::new(width as u32, height as u32); for y in 0..height { for x in 0..width { let value = values[y * width + x].clamp(0.0, 1.0); image.put_pixel(x as u32, y as u32, Luma([(value * 255.0).round() as u8])); } } image } fn apply_shading(heightmap: &[f32], width: usize, height: usize, strength: f32) -> Vec { let mut output = vec![0.0; width * height]; let lx = -1.0_f32; let ly = -1.0_f32; let lz = 1.0_f32; let l_len = (lx * lx + ly * ly + lz * lz).sqrt(); let (lx, ly, lz) = (lx / l_len, ly / l_len, lz / l_len); for y in 0..height { for x in 0..width { let left = sample_height(heightmap, width, height, x.saturating_sub(1), y); let right = sample_height(heightmap, width, height, (x + 1).min(width - 1), y); let up = sample_height(heightmap, width, height, x, y.saturating_sub(1)); let down = sample_height(heightmap, width, height, x, (y + 1).min(height - 1)); let mut dx = (right - left) * 0.5 * strength; let mut dy = (down - up) * 0.5 * strength; dx *= width as f32; dy *= height as f32; let n_len = (dx * dx + dy * dy + 1.0).sqrt(); let nx = -dx / n_len; let ny = -dy / n_len; let nz = 1.0 / n_len; let dot = nx * lx + ny * ly + nz * lz; output[y * width + x] = dot.clamp(0.0, 1.0); } } output } fn sample_height(heightmap: &[f32], width: usize, _height: usize, x: usize, y: usize) -> f32 { heightmap[y * width + x] }