feat: wire shell placeholders to backend actions #12
+76
-12
@@ -1,5 +1,15 @@
|
||||
use crate::terrain::{HeightGrid, TerrainError};
|
||||
|
||||
const OCTAVE_COUNT: usize = 4;
|
||||
const BASE_FREQUENCY: f32 = 2.0;
|
||||
const LACUNARITY: f32 = 2.0;
|
||||
const GAIN: f32 = 0.5;
|
||||
const LATTICE_SEED_STEP: u64 = 0x9E37_79B9_7F4A_7C15;
|
||||
const LATTICE_X_MUL: u64 = 0x9E37_79B9_7F4A_7C15;
|
||||
const LATTICE_Y_MUL: u64 = 0xC2B2_AE3D_27D4_EB4F;
|
||||
const FINALIZER_MUL1: u64 = 0xBF58_476D_1CE4_E5B9;
|
||||
const FINALIZER_MUL2: u64 = 0x94D0_49BB_1331_11EB;
|
||||
|
||||
/// Describes a terrain generation request.
|
||||
///
|
||||
/// Specs are immutable once constructed; identical specs always yield
|
||||
@@ -44,9 +54,9 @@ pub trait TerrainGenerator {
|
||||
|
||||
/// A deterministic, in-memory reference generator.
|
||||
///
|
||||
/// Heights are derived purely from the spec's seed and each sample's
|
||||
/// coordinates, so identical specs always produce identical grids. No noise
|
||||
/// algorithms are used yet; this is a stable baseline for CLI and rendering.
|
||||
/// Heights are derived from a small clean-room seeded value-noise fBm stack so
|
||||
/// identical specs always produce identical grids. The output stays in
|
||||
/// `[0.0, 1.0]`, which keeps later renderer and palette integration simple.
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct DeterministicTerrainGenerator;
|
||||
|
||||
@@ -56,29 +66,83 @@ impl DeterministicTerrainGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
/// Hashes seed and coordinates into a height in `[0.0, 1.0)`.
|
||||
fn sample_height(seed: u64, x: u32, y: u32) -> f32 {
|
||||
// SplitMix64-style finalizer over a coordinate-mixed seed.
|
||||
let mut h = seed
|
||||
^ (u64::from(x).wrapping_mul(0x9E37_79B9_7F4A_7C15))
|
||||
^ (u64::from(y).wrapping_mul(0xC2B2_AE3D_27D4_EB4F));
|
||||
#[inline]
|
||||
fn fade(t: f32) -> f32 {
|
||||
t * t * (3.0 - 2.0 * t)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn normalized_coord(index: u32, size: u32) -> f32 {
|
||||
if size <= 1 {
|
||||
0.0
|
||||
} else {
|
||||
index as f32 / (size - 1) as f32
|
||||
}
|
||||
}
|
||||
|
||||
/// Hashes seed and lattice coordinates into a height in `[0.0, 1.0)`.
|
||||
#[inline]
|
||||
fn lattice_value(seed: u64, x: i64, y: i64) -> f32 {
|
||||
let mut h =
|
||||
seed ^ (x as u64).wrapping_mul(LATTICE_X_MUL) ^ (y as u64).wrapping_mul(LATTICE_Y_MUL);
|
||||
h ^= h >> 30;
|
||||
h = h.wrapping_mul(0xBF58_476D_1CE4_E5B9);
|
||||
h = h.wrapping_mul(FINALIZER_MUL1);
|
||||
h ^= h >> 27;
|
||||
h = h.wrapping_mul(0x94D0_49BB_1331_11EB);
|
||||
h = h.wrapping_mul(FINALIZER_MUL2);
|
||||
h ^= h >> 31;
|
||||
// Take the top 24 bits for an exact f32 fraction in [0, 1).
|
||||
(h >> 40) as f32 / (1u64 << 24) as f32
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn value_noise(seed: u64, x: f32, y: f32) -> f32 {
|
||||
let x0 = x.floor() as i64;
|
||||
let y0 = y.floor() as i64;
|
||||
let x1 = x0 + 1;
|
||||
let y1 = y0 + 1;
|
||||
let tx = x - x0 as f32;
|
||||
let ty = y - y0 as f32;
|
||||
let sx = fade(tx);
|
||||
let sy = fade(ty);
|
||||
|
||||
let n00 = lattice_value(seed, x0, y0);
|
||||
let n10 = lattice_value(seed, x1, y0);
|
||||
let n01 = lattice_value(seed, x0, y1);
|
||||
let n11 = lattice_value(seed, x1, y1);
|
||||
|
||||
let ix0 = n00 * (1.0 - sx) + n10 * sx;
|
||||
let ix1 = n01 * (1.0 - sx) + n11 * sx;
|
||||
ix0 * (1.0 - sy) + ix1 * sy
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn fbm_height(seed: u64, x: f32, y: f32) -> f32 {
|
||||
let mut total = 0.0;
|
||||
let mut amplitude = 1.0;
|
||||
let mut frequency = BASE_FREQUENCY;
|
||||
let mut amplitude_sum = 0.0;
|
||||
|
||||
for octave in 0..OCTAVE_COUNT {
|
||||
let octave_seed = seed.wrapping_add((octave as u64).wrapping_mul(LATTICE_SEED_STEP));
|
||||
total += amplitude * value_noise(octave_seed, x * frequency, y * frequency);
|
||||
amplitude_sum += amplitude;
|
||||
amplitude *= GAIN;
|
||||
frequency *= LACUNARITY;
|
||||
}
|
||||
|
||||
total / amplitude_sum
|
||||
}
|
||||
|
||||
impl TerrainGenerator for DeterministicTerrainGenerator {
|
||||
fn generate(&self, spec: &TerrainGenerationSpec) -> Result<HeightGrid, TerrainError> {
|
||||
let width = spec.width();
|
||||
let height = spec.height();
|
||||
let mut samples = Vec::with_capacity((width as usize) * (height as usize));
|
||||
for y in 0..height {
|
||||
let ny = normalized_coord(y, height);
|
||||
for x in 0..width {
|
||||
samples.push(sample_height(spec.seed(), x, y));
|
||||
let nx = normalized_coord(x, width);
|
||||
samples.push(fbm_height(spec.seed(), nx, ny));
|
||||
}
|
||||
}
|
||||
HeightGrid::new(width, height, samples)
|
||||
|
||||
@@ -47,3 +47,32 @@ fn terrain_gen_deterministic_generator_rejects_zero_dimensions() {
|
||||
let err = TerrainGenerationSpec::new(7, 4, 0).unwrap_err();
|
||||
assert_eq!(err, TerrainError::ZeroDimension);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn terrain_gen_deterministic_generator_produces_value_noise_fbm_sample() {
|
||||
let spec = TerrainGenerationSpec::new(1337, 4, 4).expect("valid spec");
|
||||
let generator = DeterministicTerrainGenerator::new();
|
||||
|
||||
let grid = generator.generate(&spec).expect("generation succeeds");
|
||||
|
||||
let expected = [
|
||||
[0.706_783_f32, 0.390_681, 0.571_295, 0.454_375],
|
||||
[0.667_478, 0.549_327, 0.476_506, 0.335_594],
|
||||
[0.603_516, 0.451_503, 0.516_397, 0.357_081],
|
||||
[0.308_838, 0.295_558, 0.570_510, 0.666_683],
|
||||
];
|
||||
|
||||
for (y, row) in expected.iter().enumerate() {
|
||||
for (x, expected_sample) in row.iter().enumerate() {
|
||||
let actual = grid.sample(x as u32, y as u32).expect("sample in bounds");
|
||||
assert!(
|
||||
(actual - expected_sample).abs() < 1e-6,
|
||||
"mismatch at ({x}, {y}): expected {expected_sample}, got {actual}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let (min, max) = grid.min_max().expect("non-empty grid has min/max");
|
||||
assert!(min >= 0.0);
|
||||
assert!(max <= 1.0);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user