diff --git a/src/bloxel/worldgen/chunk_loading.rs b/src/bloxel/worldgen/chunk_loading.rs new file mode 100644 index 0000000..3b506a0 --- /dev/null +++ b/src/bloxel/worldgen/chunk_loading.rs @@ -0,0 +1,112 @@ +use bevy::prelude::*; + +use crate::{ + bloxel::{ + prelude::*, + storage::{ChunkedOctree, OctreeNode}, + }, + camera_controller::ControlledCamera, +}; + +const LOAD_DISTANCE: usize = 12; +const UNLOAD_DISTANCE: usize = 16; +const CHUNKS_PER_ITERATION: usize = 12; + +pub fn create_chunks_around_camera( + mut commands: Commands, + mut octree: ResMut, + camera: Single<&Transform, With>, + chunk_map: Single>, +) { + let block_pos = camera.translation.as_ivec3().into(); + let (chunk_pos, _) = ChunkPos::from_block_pos(block_pos); + + let mut create_chunk = |octree: &mut ExistingChunks, pos: ChunkPos| { + commands.entity(*chunk_map).with_child((Chunk, pos)); + octree.update(pos, |_, children, parent| { + let children = children.map(|a| a.as_slice()).unwrap_or_default(); + *parent = if children.iter().all(|c| *c == Existing::All) { + Existing::All + } else { + Existing::Some + } + }); + }; + + // Create chunk at camera's position, if it's not already. This is necessary because we + // need to "seed" the octree with a region so we have something to begin the search from. + if octree.get(chunk_pos) == Existing::None { + create_chunk(&mut octree, chunk_pos); + } + + let sqr_load_distance = (LOAD_DISTANCE * LOAD_DISTANCE) as i32; + let to_create = octree + .find(|node, exist| { + (exist != Existing::All).then(|| -node.region().distance_to_squared(chunk_pos)) + }) + .take_while(|(_, _, neg_sqr_distance)| *neg_sqr_distance > -sqr_load_distance) + .map(|(chunk_pos, _, _)| chunk_pos) + .take(CHUNKS_PER_ITERATION) // Create up to this many chunks per system iteration. + .collect::>(); + + for chunk_pos in to_create { + create_chunk(&mut octree, chunk_pos); + } +} + +pub fn destroy_chunks_away_from_camera( + mut commands: Commands, + mut octree: ResMut, + camera: Single<&Transform, With>, + chunk_map: Single<&ChunkMap>, +) { + let block_pos = camera.translation.as_ivec3().into(); + let (chunk_pos, _) = ChunkPos::from_block_pos(block_pos); + + let distance = |node: OctreeNode| -> i32 { + let (min, max) = node.region().into(); + let a = (chunk_pos - min).length_squared(); + let b = (chunk_pos - max).length_squared(); + a.max(b) + }; + + let sqr_unload_distance = (UNLOAD_DISTANCE * UNLOAD_DISTANCE) as i32; + let to_destroy = octree + .find(|node, exist| (exist != Existing::None).then(|| distance(node))) + .take_while(|(_, _, sqr_distance)| *sqr_distance > sqr_unload_distance) + .map(|(chunk_pos, _, _)| chunk_pos) + .collect::>(); + + for chunk_pos in to_destroy { + let chunk = *chunk_map.get(&chunk_pos).unwrap(); + commands.entity(chunk).despawn(); + octree.update(chunk_pos, |_, children, parent| { + let children = children.map(|a| a.as_slice()).unwrap_or_default(); + *parent = if children.iter().all(|c| *c == Existing::None) { + Existing::None + } else { + Existing::Some + } + }); + } +} + +#[derive(Resource, Deref, DerefMut)] +pub struct ExistingChunks { + octree: ChunkedOctree, +} + +impl Default for ExistingChunks { + fn default() -> Self { + let octree = ChunkedOctree::new(5); + Self { octree } + } +} + +#[derive(Default, Clone, Copy, PartialEq, Eq, Hash, Debug)] +pub enum Existing { + #[default] + None, + Some, + All, +} diff --git a/src/bloxel/worldgen/mod.rs b/src/bloxel/worldgen/mod.rs index 24a3980..8aa0ebb 100644 --- a/src/bloxel/worldgen/mod.rs +++ b/src/bloxel/worldgen/mod.rs @@ -1,18 +1,10 @@ use bevy::prelude::*; -use noise_functions::{Noise, Simplex}; -use crate::{ - bloxel::{ - prelude::*, - storage::{ChunkedOctree, OctreeNode}, - }, - camera_controller::ControlledCamera, - TerrainBlocks, -}; +mod chunk_loading; +mod terrain_shape; -const LOAD_DISTANCE: usize = 12; -const UNLOAD_DISTANCE: usize = 16; -const CHUNKS_PER_ITERATION: usize = 12; +use chunk_loading::*; +use terrain_shape::*; pub struct WorldGenPlugin; @@ -21,143 +13,11 @@ impl Plugin for WorldGenPlugin { app.init_resource::().add_systems( Update, ( - create_chunks_around_camera, destroy_chunks_away_from_camera, - generate_terrain, - ), + create_chunks_around_camera, + generate_terrain_shape, + ) + .chain(), ); } } - -fn create_chunks_around_camera( - mut commands: Commands, - mut octree: ResMut, - camera: Single<&Transform, With>, - chunk_map: Single>, -) { - let block_pos = camera.translation.as_ivec3().into(); - let (chunk_pos, _) = ChunkPos::from_block_pos(block_pos); - - let mut create_chunk = |octree: &mut ExistingChunks, pos: ChunkPos| { - commands.entity(*chunk_map).with_child((Chunk, pos)); - octree.update(pos, |_, children, parent| { - let children = children.map(|a| a.as_slice()).unwrap_or_default(); - *parent = if children.iter().all(|c| *c == Existing::All) { - Existing::All - } else { - Existing::Some - } - }); - }; - - // Create chunk at camera's position, if it's not already. This is necessary because we - // need to "seed" the octree with a region so we have something to begin the search from. - if octree.get(chunk_pos) == Existing::None { - create_chunk(&mut octree, chunk_pos); - } - - let sqr_load_distance = (LOAD_DISTANCE * LOAD_DISTANCE) as i32; - let to_create = octree - .find(|node, exist| { - (exist != Existing::All).then(|| -node.region().distance_to_squared(chunk_pos)) - }) - .take_while(|(_, _, neg_sqr_distance)| *neg_sqr_distance > -sqr_load_distance) - .map(|(chunk_pos, _, _)| chunk_pos) - .take(CHUNKS_PER_ITERATION) // Create up to this many chunks per system iteration. - .collect::>(); - - for chunk_pos in to_create { - create_chunk(&mut octree, chunk_pos); - } -} - -fn destroy_chunks_away_from_camera( - mut commands: Commands, - mut octree: ResMut, - camera: Single<&Transform, With>, - chunk_map: Single<&ChunkMap>, -) { - let block_pos = camera.translation.as_ivec3().into(); - let (chunk_pos, _) = ChunkPos::from_block_pos(block_pos); - - let distance = |node: OctreeNode| -> i32 { - let (min, max) = node.region().into(); - let a = (chunk_pos - min).length_squared(); - let b = (chunk_pos - max).length_squared(); - a.max(b) - }; - - let sqr_unload_distance = (UNLOAD_DISTANCE * UNLOAD_DISTANCE) as i32; - let to_destroy = octree - .find(|node, exist| (exist != Existing::None).then(|| distance(node))) - .take_while(|(_, _, sqr_distance)| *sqr_distance > sqr_unload_distance) - .map(|(chunk_pos, _, _)| chunk_pos) - .collect::>(); - - for chunk_pos in to_destroy { - let chunk = *chunk_map.get(&chunk_pos).unwrap(); - commands.entity(chunk).despawn(); - octree.update(chunk_pos, |_, children, parent| { - let children = children.map(|a| a.as_slice()).unwrap_or_default(); - *parent = if children.iter().all(|c| *c == Existing::None) { - Existing::None - } else { - Existing::Some - } - }); - } -} - -fn generate_terrain( - mut commands: Commands, - terrain_blocks: Option>, - chunks_without_data: Query<(Entity, &ChunkPos), (With, Without)>, -) { - let Some(terrain) = terrain_blocks else { - return; - }; - - let slices = [terrain.rock, terrain.dirt, terrain.grass, terrain.sand]; - - for (entity, chunk_pos) in chunks_without_data.iter() { - let mut data = ChunkData::new(terrain.air); - data.update(|relative, _| { - let block_pos = chunk_pos.to_block_pos(relative); - let float_pos = IVec3::from(block_pos).as_vec3(); - - let bias = ((float_pos.y + 32.) / 64.).clamp(-0.25, 1.); - let sample = Simplex - .fbm(3, 0.65, 2.0) - .weighted(0.4) - .frequency(0.01) - .sample3(float_pos); - - if sample > bias { - slices[relative.y as usize % slices.len()] - } else { - terrain.air - } - }); - commands.entity(entity).insert(data); - } -} - -#[derive(Resource, Deref, DerefMut)] -pub struct ExistingChunks { - octree: ChunkedOctree, -} - -impl Default for ExistingChunks { - fn default() -> Self { - let octree = ChunkedOctree::new(5); - Self { octree } - } -} - -#[derive(Default, Clone, Copy, PartialEq, Eq, Hash, Debug)] -pub enum Existing { - #[default] - None, - Some, - All, -} diff --git a/src/bloxel/worldgen/terrain_shape.rs b/src/bloxel/worldgen/terrain_shape.rs new file mode 100644 index 0000000..f31cdc7 --- /dev/null +++ b/src/bloxel/worldgen/terrain_shape.rs @@ -0,0 +1,40 @@ +use bevy::prelude::*; +use noise_functions::{Noise, Simplex}; + +use crate::{bloxel::prelude::*, TerrainBlocks}; + +pub fn generate_terrain_shape( + mut commands: Commands, + terrain_blocks: Option>, + chunks_without_data: Query<(Entity, &ChunkPos), (With, Without)>, +) { + let Some(terrain) = terrain_blocks else { + return; + }; + + let slices = [terrain.rock, terrain.dirt, terrain.grass, terrain.sand]; + + for (entity, chunk_pos) in chunks_without_data.iter() { + let mut data = ChunkData::new(terrain.air); + + data.update(|relative, _| { + let block_pos = chunk_pos.to_block_pos(relative); + let float_pos = IVec3::from(block_pos).as_vec3(); + + let bias = ((float_pos.y + 32.) / 64.).clamp(-0.25, 1.); + let sample = Simplex + .fbm(3, 0.65, 2.0) + .weighted(0.4) + .frequency(0.01) + .sample3(float_pos); + + if sample > bias { + slices[relative.y as usize % slices.len()] + } else { + terrain.air + } + }); + + commands.entity(entity).insert(data); + } +}