synvael/crates/shared/src/generator.rs

88 lines
3 KiB
Rust

// SPDX-License-Identifier: AGPL-3.0-only
//! Procedural generation logic for the voxel world.
use noise::{NoiseFn, Perlin};
use serde::{Deserialize, Serialize};
use crate::world::{BlockId, CHUNK_SIZE, Chunk, ChunkPos};
/// Configuration parameters for deterministic world generation.
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct WorldGenConfig {
/// The base height around which terrain features are generated.
pub base_height: u32,
/// The scale factor applied to the noise coordinates. Smaller values create wider features.
pub noise_scale: f64,
/// The block identifier used for the top layer of the terrain.
pub surface_block: BlockId,
/// The block identifier used for the layers immediately below the surface.
pub subsurface_block: BlockId,
/// The block identifier used for deep underground layers.
pub stone_block: BlockId,
}
/// A deterministic terrain generator that produces voxel chunks.
pub struct VoxelGenerator {
/// The configuration parameters guiding the generation.
pub config: WorldGenConfig,
noise: Perlin,
}
impl VoxelGenerator {
/// Initializes a new voxel generator with the specified configuration and seed.
#[must_use]
pub fn new(config: WorldGenConfig, seed: u32) -> Self {
Self {
config,
noise: Perlin::new(seed),
}
}
/// Generates a complete voxel chunk for the specified position.
#[must_use]
#[expect(
clippy::cast_possible_wrap,
clippy::cast_possible_truncation,
reason = "chunk and voxel coordinates stay within the ranges these casts assume"
)]
pub fn generate_chunk(&self, pos: ChunkPos) -> Chunk {
let mut chunk = Chunk::default();
for x in 0..CHUNK_SIZE {
for z in 0..CHUNK_SIZE {
let global_x = (pos.x * CHUNK_SIZE as i32) + x as i32;
let global_z = (pos.z * CHUNK_SIZE as i32) + z as i32;
let noise_val = self.noise.get([
f64::from(global_x) * self.config.noise_scale,
f64::from(global_z) * self.config.noise_scale,
]);
let amplitude = 15.0;
let target_height = self.config.base_height as i32 + (noise_val * amplitude) as i32;
for y in 0..CHUNK_SIZE {
let global_y = (pos.y * CHUNK_SIZE as i32) + y as i32;
let block = if global_y > target_height {
BlockId::AIR
} else if global_y == target_height {
self.config.surface_block
} else if global_y > target_height - 3 {
self.config.subsurface_block
} else {
self.config.stone_block
};
if block != BlockId::AIR {
chunk.set(x, y, z, block);
}
}
}
}
chunk
}
}