use crate::gfx::chunk_coord::ChunkCoord;
use alloc::vec;
use alloc::vec::Vec;
#[derive(Clone, Copy, Debug)]
pub struct ChunkBlockType {
pub solid: bool,
pub uv_top: [f32; 4],
pub uv_bottom: [f32; 4],
pub uv_side: [f32; 4],
}
pub struct ChunkGenerator {
seed: u64,
chunk_blocks: [u32; 3],
surface_idx: u32,
subsurface_idx: u32,
}
const OCTAVES: [(i32, f32); 3] = [(64, 1.0), (32, 0.5), (16, 0.25)];
impl ChunkGenerator {
pub fn new(seed: u64, chunk_blocks: [u32; 3], palette_len: u32) -> Self {
let surface_idx = if palette_len > 1 { 1 } else { 0 };
let subsurface_idx = if palette_len > 2 { 2 } else { surface_idx };
Self {
seed,
chunk_blocks: [
chunk_blocks[0].max(1),
chunk_blocks[1].max(1),
chunk_blocks[2].max(1),
],
surface_idx,
subsurface_idx,
}
}
pub fn generate(&self, coord: ChunkCoord) -> Vec<u32> {
let [dx, dy, dz] = [
self.chunk_blocks[0] as usize,
self.chunk_blocks[1] as usize,
self.chunk_blocks[2] as usize,
];
let mut blocks = vec![0u32; dx * dy * dz];
let base_x = coord.x * self.chunk_blocks[0] as i32;
let base_z = coord.z * self.chunk_blocks[2] as i32;
for z in 0..dz {
for x in 0..dx {
let wx = base_x + x as i32;
let wz = base_z + z as i32;
let height = self.surface_height(wx, wz, dy as i32);
for y in 0..dy {
let yi = y as i32;
let id = if yi > height {
0 } else if yi == height {
self.surface_idx
} else {
self.subsurface_idx
};
blocks[x + y * dx + z * dx * dy] = id;
}
}
}
blocks
}
pub fn surface_height_world(&self, wx: i32, wz: i32) -> i32 {
self.surface_height(wx, wz, self.chunk_blocks[1] as i32)
}
pub fn surface_palette_index(&self) -> u32 {
self.surface_idx
}
fn surface_height(&self, wx: i32, wz: i32, dy: i32) -> i32 {
let n = self.combined_noise(wx, wz); let base = dy as f32 * 0.45;
let amplitude = dy as f32 * 0.30;
let h = base + (n - 0.5) * 2.0 * amplitude;
(h as i32).clamp(0, dy - 1)
}
fn combined_noise(&self, wx: i32, wz: i32) -> f32 {
let mut sum = 0.0;
let mut weight_sum = 0.0;
for (octave, &(feature, weight)) in OCTAVES.iter().enumerate() {
let octave_seed = self.seed.wrapping_add(octave as u64 * 0x9E37_79B9);
sum += weight * value_noise(octave_seed, wx, wz, feature);
weight_sum += weight;
}
if weight_sum > 0.0 {
sum / weight_sum
} else {
0.5
}
}
}
fn value_noise(seed: u64, wx: i32, wz: i32, feature: i32) -> f32 {
let feature = feature.max(1);
let cell_x = wx.div_euclid(feature);
let cell_z = wz.div_euclid(feature);
let tx = wx.rem_euclid(feature) as f32 / feature as f32;
let tz = wz.rem_euclid(feature) as f32 / feature as f32;
let v00 = hash01(seed, cell_x, cell_z);
let v10 = hash01(seed, cell_x + 1, cell_z);
let v01 = hash01(seed, cell_x, cell_z + 1);
let v11 = hash01(seed, cell_x + 1, cell_z + 1);
let sx = smoothstep(tx);
let sz = smoothstep(tz);
let a = v00 + (v10 - v00) * sx;
let b = v01 + (v11 - v01) * sx;
a + (b - a) * sz
}
fn smoothstep(t: f32) -> f32 {
t * t * (3.0 - 2.0 * t)
}
fn hash01(seed: u64, x: i32, z: i32) -> f32 {
let mut h = seed;
h ^= (x as i64 as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15);
h = h.wrapping_mul(0xBF58_476D_1CE4_E5B9);
h ^= (z as i64 as u64).wrapping_mul(0xC2B2_AE3D_27D4_EB4F);
h ^= h >> 31;
h = h.wrapping_mul(0x94D0_49BB_1331_11EB);
h ^= h >> 31;
(h >> 40) as f32 / (1u64 << 24) as f32
}
#[cfg(test)]
mod tests {
use super::*;
fn make_gen() -> ChunkGenerator {
ChunkGenerator::new(1234, [16, 24, 16], 3)
}
#[test]
fn generate_produces_the_expected_block_count() {
let blocks = make_gen().generate(ChunkCoord::new(0, 0));
assert_eq!(blocks.len(), 16 * 24 * 16);
}
#[test]
fn generation_is_deterministic() {
let a = make_gen().generate(ChunkCoord::new(3, -2));
let b = make_gen().generate(ChunkCoord::new(3, -2));
assert_eq!(a, b);
}
#[test]
fn different_seeds_produce_different_worlds() {
let a = ChunkGenerator::new(1, [16, 24, 16], 3).generate(ChunkCoord::new(0, 0));
let b = ChunkGenerator::new(2, [16, 24, 16], 3).generate(ChunkCoord::new(0, 0));
assert_ne!(a, b);
}
#[test]
fn columns_are_solid_below_the_surface_and_air_above() {
let g = make_gen();
let [dx, dy, dz] = [16usize, 24usize, 16usize];
let blocks = g.generate(ChunkCoord::new(0, 0));
for z in 0..dz {
for x in 0..dx {
let mut top = None;
for y in (0..dy).rev() {
if blocks[x + y * dx + z * dx * dy] != 0 {
top = Some(y);
break;
}
}
let top = top.expect("every column has a solid block");
for y in 0..dy {
let solid = blocks[x + y * dx + z * dx * dy] != 0;
assert_eq!(solid, y <= top, "column ({x},{z}) y={y}");
}
}
}
}
#[test]
fn terrain_keys_on_world_coordinates() {
let g = make_gen();
let [dx, dy, dz] = [16i32, 24i32, 16i32];
let chunk = ChunkCoord::new(1, -2);
let blocks = g.generate(chunk);
let base_x = chunk.x * dx;
let base_z = chunk.z * dz;
for z in 0..dz as usize {
for x in 0..dx as usize {
let mut top = 0;
for y in (0..dy as usize).rev() {
if blocks[x + y * dx as usize + z * (dx * dy) as usize] != 0 {
top = y as i32;
break;
}
}
let expected = g.surface_height(base_x + x as i32, base_z + z as i32, dy);
assert_eq!(top, expected, "column ({x},{z}) height");
}
}
}
#[test]
fn value_noise_stays_in_unit_range() {
for wx in -40..40 {
for wz in -40..40 {
let n = value_noise(99, wx, wz, 16);
assert!((0.0..1.0).contains(&n), "noise {n} out of range");
}
}
}
#[test]
fn short_palette_falls_back_to_the_surface_index() {
let g = ChunkGenerator::new(5, [4, 8, 4], 2);
let blocks = g.generate(ChunkCoord::new(0, 0));
assert!(blocks.iter().all(|&b| b <= 1));
assert!(blocks.contains(&1));
}
}