feat: wire shell placeholders to backend actions #12
+76
-12
@@ -1,5 +1,15 @@
|
|||||||
use crate::terrain::{HeightGrid, TerrainError};
|
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.
|
/// Describes a terrain generation request.
|
||||||
///
|
///
|
||||||
/// Specs are immutable once constructed; identical specs always yield
|
/// Specs are immutable once constructed; identical specs always yield
|
||||||
@@ -44,9 +54,9 @@ pub trait TerrainGenerator {
|
|||||||
|
|
||||||
/// A deterministic, in-memory reference generator.
|
/// A deterministic, in-memory reference generator.
|
||||||
///
|
///
|
||||||
/// Heights are derived purely from the spec's seed and each sample's
|
/// Heights are derived from a small clean-room seeded value-noise fBm stack so
|
||||||
/// coordinates, so identical specs always produce identical grids. No noise
|
/// identical specs always produce identical grids. The output stays in
|
||||||
/// algorithms are used yet; this is a stable baseline for CLI and rendering.
|
/// `[0.0, 1.0]`, which keeps later renderer and palette integration simple.
|
||||||
#[derive(Debug, Clone, Copy, Default)]
|
#[derive(Debug, Clone, Copy, Default)]
|
||||||
pub struct DeterministicTerrainGenerator;
|
pub struct DeterministicTerrainGenerator;
|
||||||
|
|
||||||
@@ -56,29 +66,83 @@ impl DeterministicTerrainGenerator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Hashes seed and coordinates into a height in `[0.0, 1.0)`.
|
#[inline]
|
||||||
fn sample_height(seed: u64, x: u32, y: u32) -> f32 {
|
fn fade(t: f32) -> f32 {
|
||||||
// SplitMix64-style finalizer over a coordinate-mixed seed.
|
t * t * (3.0 - 2.0 * t)
|
||||||
let mut h = seed
|
}
|
||||||
^ (u64::from(x).wrapping_mul(0x9E37_79B9_7F4A_7C15))
|
|
||||||
^ (u64::from(y).wrapping_mul(0xC2B2_AE3D_27D4_EB4F));
|
#[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 >> 30;
|
||||||
h = h.wrapping_mul(0xBF58_476D_1CE4_E5B9);
|
h = h.wrapping_mul(FINALIZER_MUL1);
|
||||||
h ^= h >> 27;
|
h ^= h >> 27;
|
||||||
h = h.wrapping_mul(0x94D0_49BB_1331_11EB);
|
h = h.wrapping_mul(FINALIZER_MUL2);
|
||||||
h ^= h >> 31;
|
h ^= h >> 31;
|
||||||
// Take the top 24 bits for an exact f32 fraction in [0, 1).
|
// Take the top 24 bits for an exact f32 fraction in [0, 1).
|
||||||
(h >> 40) as f32 / (1u64 << 24) as f32
|
(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 {
|
impl TerrainGenerator for DeterministicTerrainGenerator {
|
||||||
fn generate(&self, spec: &TerrainGenerationSpec) -> Result<HeightGrid, TerrainError> {
|
fn generate(&self, spec: &TerrainGenerationSpec) -> Result<HeightGrid, TerrainError> {
|
||||||
let width = spec.width();
|
let width = spec.width();
|
||||||
let height = spec.height();
|
let height = spec.height();
|
||||||
let mut samples = Vec::with_capacity((width as usize) * (height as usize));
|
let mut samples = Vec::with_capacity((width as usize) * (height as usize));
|
||||||
for y in 0..height {
|
for y in 0..height {
|
||||||
|
let ny = normalized_coord(y, height);
|
||||||
for x in 0..width {
|
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)
|
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();
|
let err = TerrainGenerationSpec::new(7, 4, 0).unwrap_err();
|
||||||
assert_eq!(err, TerrainError::ZeroDimension);
|
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