From 3d8e7e23a66a8179b139c662d1e5445ca61b87a9 Mon Sep 17 00:00:00 2001 From: Mark Poliakov Date: Sun, 27 Apr 2025 19:22:16 +0300 Subject: [PATCH] Test feature generation --- assets/blocks/iron_ore.json | 5 + assets/blocks/leaves.json | 5 + assets/blocks/log.json | 5 + assets/textures/blocks/iron_ore.png | Bin 0 -> 2008 bytes assets/textures/blocks/leaves.png | Bin 0 -> 2456 bytes assets/textures/blocks/log_side.png | Bin 0 -> 347 bytes src/main.rs | 2 +- src/render/mesh.rs | 20 +- src/world/chunk.rs | 20 ++ src/world/feature.rs | 467 ++++++++++++++++++++++++++++ src/world/generator.rs | 5 +- src/world/level.rs | 244 +++++++++++++-- src/world/mod.rs | 1 + 13 files changed, 746 insertions(+), 28 deletions(-) create mode 100644 assets/blocks/iron_ore.json create mode 100644 assets/blocks/leaves.json create mode 100644 assets/blocks/log.json create mode 100644 assets/textures/blocks/iron_ore.png create mode 100644 assets/textures/blocks/leaves.png create mode 100644 assets/textures/blocks/log_side.png create mode 100644 src/world/feature.rs diff --git a/assets/blocks/iron_ore.json b/assets/blocks/iron_ore.json new file mode 100644 index 0000000..2ff96a6 --- /dev/null +++ b/assets/blocks/iron_ore.json @@ -0,0 +1,5 @@ +{ + "model": { + "simple": "iron_ore" + } +} diff --git a/assets/blocks/leaves.json b/assets/blocks/leaves.json new file mode 100644 index 0000000..880c8c4 --- /dev/null +++ b/assets/blocks/leaves.json @@ -0,0 +1,5 @@ +{ + "model": { + "simple": "leaves" + } +} diff --git a/assets/blocks/log.json b/assets/blocks/log.json new file mode 100644 index 0000000..02ae5c2 --- /dev/null +++ b/assets/blocks/log.json @@ -0,0 +1,5 @@ +{ + "model": { + "simple": "log_side" + } +} diff --git a/assets/textures/blocks/iron_ore.png b/assets/textures/blocks/iron_ore.png new file mode 100644 index 0000000000000000000000000000000000000000..843542a90524b95ee7a63a259108e29c00671e95 GIT binary patch literal 2008 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, coords: ChunkCoords, - world: &World, + world: &mut World, ) -> Result, Error> { - match self.chunks.entry(coords) { - Entry::Occupied(entry) => Ok(Some(entry.into_mut())), - Entry::Vacant(entry) => { - if let Some(chunk) = world.get(coords) { + if let Some(chunk) = world.get_mut(coords) { + if chunk.clear_dirty() { + self.chunks.remove(&coords); + } + + match self.chunks.entry(coords) { + Entry::Occupied(entry) => Ok(Some(entry.into_mut())), + Entry::Vacant(entry) => { let mesh = ChunkMesh::from_chunk( allocators, &self.block_model_registry, @@ -59,10 +63,12 @@ impl WorldMeshState { chunk, )?; Ok(Some(entry.insert(mesh))) - } else { - Ok(None) } } + } else { + self.chunks.remove(&coords); + + Ok(None) } } } diff --git a/src/world/chunk.rs b/src/world/chunk.rs index 37dc654..fa5a018 100644 --- a/src/world/chunk.rs +++ b/src/world/chunk.rs @@ -3,6 +3,7 @@ use super::NeighborQuery; pub struct Chunk { blocks: [u8; Self::SIZE * Self::SIZE * Self::HEIGHT], height_map: [u32; Self::SIZE * Self::SIZE], + dirty: bool, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -12,6 +13,7 @@ pub struct ChunkCoords { } impl ChunkCoords { + #[inline] pub const fn new(x: i32, z: i32) -> Self { Self { x, z } } @@ -24,6 +26,12 @@ impl ChunkCoords { let (cx, cz) = self.origin_block(); (cx + x as i32, cz + z as i32) } + + pub fn from_block_coords(x: i32, z: i32) -> Self { + let cx = if x < 0 { x - Chunk::SIZE as i32 + 1 } else { x } / Chunk::SIZE as i32; + let cz = if z < 0 { z - Chunk::SIZE as i32 + 1 } else { z } / Chunk::SIZE as i32; + Self::new(cx, cz) + } } impl Chunk { @@ -34,9 +42,16 @@ impl Chunk { Self { blocks: [0; Self::SIZE * Self::SIZE * Self::HEIGHT], height_map: [0; Self::SIZE * Self::SIZE], + dirty: false, } } + pub fn clear_dirty(&mut self) -> bool { + let dirty = self.dirty; + self.dirty = false; + dirty + } + pub fn fill_layer u8>(&mut self, y: u32, f: F) { for x in 0..Self::SIZE as u32 { for z in 0..Self::SIZE as u32 { @@ -50,7 +65,12 @@ impl Chunk { } pub fn set_id(&mut self, x: u32, y: u32, z: u32, id: u8) { + if x >= Self::SIZE as u32 || z >= Self::SIZE as u32 || y >= Self::HEIGHT as u32 { + panic!("Invalid coords: {x}, {y}, {z}"); + } + self.blocks[Self::to_index(x, y, z)] = id; + self.dirty = true; if id != 0 && y >= self.top_height_at(x, z) { self.set_top_height_at(x, z, y + 1); diff --git a/src/world/feature.rs b/src/world/feature.rs new file mode 100644 index 0000000..12d6f83 --- /dev/null +++ b/src/world/feature.rs @@ -0,0 +1,467 @@ +use std::{ + mem, + ops::{Add, Deref, Range}, +}; + +use super::{level::BoundingBox, Chunk, ChunkCoords}; + +pub enum FeaturePlacementFilter { + ReplaceAll, + ReplaceAir, + Replace(u8), +} + +pub struct FeatureLayer { + x_range: Range, + z_range: Range, + content: Vec, +} + +pub struct FeatureBuilder { + y_range: Range, + layers: Vec, +} + +pub struct Feature { + width: u32, + height: u32, + depth: u32, + filter: FeaturePlacementFilter, + content: Vec, +} + +pub struct PositionedFeature { + inner: Feature, + x: i32, + y: i32, + z: i32, +} + +impl Deref for PositionedFeature { + type Target = Feature; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl FeaturePlacementFilter { + pub fn should_place(&self, from: u8) -> bool { + match self { + Self::ReplaceAir => from == 0, + Self::ReplaceAll => true, + &Self::Replace(id) => from == id, + } + } +} + +impl PositionedFeature { + pub fn aabb(&self) -> BoundingBox { + BoundingBox { + x: self.x, + y: self.y, + z: self.z, + w: self.width as i32, + h: self.height as i32, + d: self.depth as i32, + } + } + + pub fn place(&self, coords: ChunkCoords, chunk: &mut Chunk) { + let in_chunk_aabb = self.aabb().in_chunk(coords); + let Some(in_chunk_aabb) = in_chunk_aabb else { + return; + }; + + let (sx, sz) = coords.origin_block(); + + for ix in 0..in_chunk_aabb.w { + // Coords in chunk + let cx = ix + in_chunk_aabb.x; + // Coords in feature + let fx = cx as i32 + sx - self.x; + assert!(fx >= 0); + assert!(fx < self.width as i32); + + for iy in 0..in_chunk_aabb.h { + let cy = iy + in_chunk_aabb.y; + + for iz in 0..in_chunk_aabb.d { + let cz = iz + in_chunk_aabb.z; + let fz = cz as i32 + sz - self.z; + assert!(fz >= 0); + assert!(fz < self.depth as i32); + + let chunk_id = chunk.get_id(cx, cy, cz); + let feature_id = self.get(fx as u32, iy, fz as u32); + + if self.filter.should_place(chunk_id) { + chunk.set_id(cx, cy, cz, feature_id); + } + } + } + } + } +} + +impl Feature { + pub fn position(self, x: i32, y: i32, z: i32) -> PositionedFeature { + PositionedFeature { + inner: self, + x, + y, + z, + } + } + + pub fn position_centered_horizontally(self, x: i32, y: i32, z: i32) -> PositionedFeature { + let x = x - self.width as i32 / 2; + let z = z - self.width as i32 / 2; + PositionedFeature { + inner: self, + x, + y, + z, + } + } + + pub fn get(&self, x: u32, y: u32, z: u32) -> u8 { + let i = x + (z + y * self.depth) * self.width; + self.content[i as usize] + } +} + +impl FeatureLayer { + pub fn new() -> Self { + Self { + x_range: 0..0, + z_range: 0..0, + content: vec![], + } + } + + #[inline] + pub fn width(&self) -> u32 { + range_width(&self.x_range) + } + + #[inline] + pub fn depth(&self) -> u32 { + range_width(&self.z_range) + } + + #[inline] + pub fn size(&self) -> (u32, u32) { + (self.width(), self.depth()) + } + + pub fn set(&mut self, x: i32, z: i32, id: u8) { + self.adjust_for_placement(x, z); + let i = self.index(x, z); + self.content[i] = id; + } + + pub fn replace(&mut self, x: i32, z: i32, from: u8, to: u8) { + self.adjust_for_placement(x, z); + if self.get(x, z) == from { + self.set(x, z, to); + } + } + + pub fn get(&self, x: i32, z: i32) -> u8 { + self.content[self.index(x, z)] + } + + pub fn try_get(&self, x: i32, z: i32) -> Option { + if !self.x_range.contains(&x) || !self.z_range.contains(&z) { + None + } else { + Some(self.get(x, z)) + } + } + + pub fn reserve(&mut self, x_range: Range, z_range: Range) { + let new_x_range = x_range.start.min(self.x_range.start)..x_range.end.max(self.x_range.end); + let new_z_range = z_range.start.min(self.z_range.start)..z_range.end.max(self.z_range.end); + + if new_x_range == self.x_range && new_z_range == self.z_range { + return; + } + + let new_width = range_width(&new_x_range); + let new_depth = range_width(&new_z_range); + + let old_width = self.width(); + let old_depth = self.depth(); + + let shift_x = (self.x_range.start - new_x_range.start) as u32; + let shift_z = (self.z_range.start - new_z_range.start) as u32; + + let old = mem::replace(&mut self.content, vec![0; (new_width * new_depth) as usize]); + + for abs_z in 0..old_depth { + let new_abs_z = shift_z + abs_z; + let abs_x = 0..old_width as usize; + let new_abs_x = advance_range(&abs_x, shift_x as usize); + + let old_i = advance_range(&abs_x, (abs_z * old_width) as usize); + let new_i = advance_range(&new_abs_x, (new_abs_z * new_width) as usize); + + self.content[new_i].copy_from_slice(&old[old_i]); + } + + self.x_range = new_x_range; + self.z_range = new_z_range; + } + + pub fn adjust_for_placement(&mut self, x: i32, z: i32) { + // Start x,z ranges: 0..0 + let new_x_range = adjust_range(x, &self.x_range); + let new_z_range = adjust_range(z, &self.z_range); + + self.reserve(new_x_range, new_z_range); + } + + fn index(&self, x: i32, z: i32) -> usize { + assert!(self.x_range.contains(&x)); + assert!(self.z_range.contains(&z)); + + let abs_x = (x - self.x_range.start) as usize; + let abs_z = (z - self.z_range.start) as usize; + + abs_x + abs_z * self.width() as usize + } +} + +impl FeatureBuilder { + pub fn new() -> Self { + Self { + y_range: 0..0, + layers: vec![], + } + } + + pub fn height(&self) -> u32 { + (self.y_range.end - self.y_range.start) as u32 + } + + pub fn reserve_height(&mut self, y_range: Range) { + let new_y_range = y_range.start.min(self.y_range.start)..y_range.end.max(self.y_range.end); + + if new_y_range == self.y_range { + return; + } + + let new_height = range_width(&new_y_range); + + let shift_y = (self.y_range.start - new_y_range.start) as usize; + + // -1..3 -> -2..3, shift_y = 1 + // [??, -1, 0, 1, 2] + // [-2, -1, 0, 1, 2] + self.layers + .resize_with(new_height as usize, FeatureLayer::new); + self.layers.rotate_right(shift_y); + self.y_range = new_y_range; + } + + pub fn adjust_height_for_placement(&mut self, y: i32) { + let new_y_range = adjust_range(y, &self.y_range); + self.reserve_height(new_y_range); + } + + pub fn set(&mut self, x: i32, y: i32, z: i32, id: u8) { + self.adjust_height_for_placement(y); + let i = (y - self.y_range.start) as usize; + self.layers[i].set(x, z, id); + } + + pub fn replace(&mut self, x: i32, y: i32, z: i32, from: u8, to: u8) { + self.adjust_height_for_placement(y); + let i = (y - self.y_range.start) as usize; + self.layers[i].replace(x, z, from, to); + } + + pub fn get(&self, x: i32, y: i32, z: i32) -> u8 { + assert!(self.y_range.contains(&y)); + self.layers[(y - self.y_range.start) as usize].get(x, z) + } + + pub fn build(self, filter: FeaturePlacementFilter) -> Feature { + assert!(!self.layers.is_empty()); + + let x_range = largest_range_bounds(self.layers.iter().map(|l| &l.x_range)); + let z_range = largest_range_bounds(self.layers.iter().map(|l| &l.z_range)); + let width = range_width(&x_range); + let depth = range_width(&z_range); + let height = self.layers.len() as u32; + + let mut content = vec![0; (width * depth) as usize * self.layers.len()]; + + for (y, layer) in self.layers.into_iter().enumerate() { + for x in x_range.clone() { + for z in z_range.clone() { + let id = layer.try_get(x, z).unwrap_or(0); + + let ix = (x - x_range.start) as usize; + let iz = (z - z_range.start) as usize; + let i = ix + (iz + y * depth as usize) * width as usize; + + content[i] = id; + } + } + } + + Feature { + width, + height, + depth, + content, + filter, + } + } +} + +fn largest_range_bounds<'a, I: IntoIterator>>(ranges: I) -> Range { + let mut result = 0..0; + for range in ranges { + if range.start < result.start { + result.start = range.start; + } + if range.end > result.end { + result.end = range.end; + } + } + result +} + +fn advance_range>(range: &Range, x: T) -> Range { + range.start + x..range.end + x +} + +#[inline] +fn adjust_range(x: i32, axis: &Range) -> Range { + let mut new_x = axis.clone(); + if x < axis.start { + new_x.start = x; + } else if x >= axis.end { + new_x.end = x + 1; + } + new_x +} + +#[inline] +fn range_width(axis: &Range) -> u32 { + assert!(axis.end >= axis.start); + (axis.end - axis.start) as u32 +} + +#[cfg(test)] +mod tests { + use super::{FeatureBuilder, FeatureLayer}; + + #[test] + fn featuer_build() { + let mut builder = FeatureBuilder::new(); + + builder.set(0, 0, 0, 1); + builder.set(0, 1, 0, 1); + for y in 2..5 { + for x in -1..=1 { + for z in -1..=1 { + let id = if x == 0 && z == 0 { 3 } else { 2 }; + builder.set(x, y, z, id); + } + } + } + + // Should yield a 3x5x3 feature + let feature = builder.build(); + assert_eq!(feature.width, 3); + assert_eq!(feature.height, 5); + assert_eq!(feature.depth, 3); + assert_eq!( + &feature.content[..], + &[ + // y = 0 + 0, 0, 0, // z = -1 + 0, 1, 0, // z = 0 + 0, 0, 0, // z = 1 + // y = 1 + 0, 0, 0, // z = -1 + 0, 1, 0, // z = 0 + 0, 0, 0, // z = 1 + // y = 2 + 2, 2, 2, // z = -1 + 2, 3, 2, // z = 0 + 2, 2, 2, // z = 1 + // y = 3 + 2, 2, 2, // z = -1 + 2, 3, 2, // z = 0 + 2, 2, 2, // z = 1 + // y = 4 + 2, 2, 2, // z = -1 + 2, 3, 2, // z = 0 + 2, 2, 2, // z = 1 + ] + ); + } + + #[test] + fn feature_layer_resize() { + let mut layer = FeatureLayer::new(); + assert_eq!(layer.size(), (0, 0)); + + // 0x0 -> 1x1 + layer.set(0, 0, 1); + assert_eq!(layer.size(), (1, 1)); + assert_eq!(layer.get(0, 0), 1); + + // 1x1 -> 3x1 + layer.set(-2, 0, 2); + assert_eq!(layer.size(), (3, 1)); + assert_eq!(layer.get(0, 0), 1); + assert_eq!(layer.get(-1, 0), 0); + assert_eq!(layer.get(-2, 0), 2); + + // 3x1 -> 3x2 + layer.set(-1, -1, 3); + assert_eq!(layer.size(), (3, 2)); + assert_eq!( + &layer.content[..], + &[ + 0, 3, 0, // z = -1 + 2, 0, 1, // z = 0 + ] + ); + + // 3x2 -> 4x4 + layer.set(-3, -3, 4); + assert_eq!(layer.size(), (4, 4)); + assert_eq!( + &layer.content[..], + &[ + 4, 0, 0, 0, // z = -3 + 0, 0, 0, 0, // z = -2 + 0, 0, 3, 0, // z = -1 + 0, 2, 0, 1, // z = 0 + ] + ); + + // 4x4 -> 5x6 + layer.set(1, 2, 5); + assert_eq!(layer.size(), (5, 6)); + assert_eq!( + &layer.content[..], + &[ + 4, 0, 0, 0, 0, // z = -3 + 0, 0, 0, 0, 0, // z = -2 + 0, 0, 3, 0, 0, // z = -1 + 0, 2, 0, 1, 0, // z = 0 + 0, 0, 0, 0, 0, // z = 1 + 0, 0, 0, 0, 5, // z = 2 + ] + ); + } +} + diff --git a/src/world/generator.rs b/src/world/generator.rs index 53f82b1..6c28f19 100644 --- a/src/world/generator.rs +++ b/src/world/generator.rs @@ -17,7 +17,7 @@ impl ChunkGenerator for NoiseChunkGenerator { ) { let grass = block_registry.get("grass").unwrap_or(0); let dirt = block_registry.get("dirt").unwrap_or(0); - let stone = block_registry.get("stone").unwrap_or(0); + // let stone = block_registry.get("stone").unwrap_or(0); for ix in 0..Chunk::SIZE as u32 { for iz in 0..Chunk::SIZE as u32 { @@ -37,7 +37,8 @@ impl ChunkGenerator for NoiseChunkGenerator { let id = if y + 1 == h { grass } else if h - y > stone_depth { - stone + 0 + // stone } else { dirt }; diff --git a/src/world/level.rs b/src/world/level.rs index 1387bbf..9cb8686 100644 --- a/src/world/level.rs +++ b/src/world/level.rs @@ -1,20 +1,95 @@ -use std::collections::HashMap; +use std::{ + collections::{HashMap, HashSet}, + sync::Arc, +}; use glam::Vec3; use crate::render::asset::block::BlockRegistry; -use super::{Chunk, ChunkCoords, ChunkGenerator}; +use super::{ + feature::{FeatureBuilder, FeaturePlacementFilter, PositionedFeature}, + Chunk, ChunkCoords, ChunkGenerator, +}; pub struct World { chunks: HashMap, block_registry: BlockRegistry, + + // populated_chunk_features: HashSet, + unpopulated_chunks: HashSet, + pending_chunk_features: HashMap>>, +} + +#[derive(Clone, Copy, Debug)] +pub struct BoundingBox { + pub x: i32, + pub y: i32, + pub z: i32, + pub w: i32, + pub h: i32, + pub d: i32, +} + +#[derive(Clone, Copy, Debug)] +pub struct InChunkBoundingBox { + pub x: u32, + pub y: u32, + pub z: u32, + pub w: u32, + pub h: u32, + pub d: u32, +} + +impl BoundingBox { + pub fn map_chunks(self, mut f: F) { + let start = ChunkCoords::from_block_coords(self.x, self.z); + let end = ChunkCoords::from_block_coords( + self.x + self.w + Chunk::SIZE as i32 - 1, + self.z + self.d + Chunk::SIZE as i32 - 1, + ); + + for cx in start.x..end.x { + for cz in start.z..end.z { + f(ChunkCoords::new(cx, cz)); + } + } + } + + pub fn in_chunk(self, chunk: ChunkCoords) -> Option { + let cx = chunk.x * Chunk::SIZE as i32; + let cz = chunk.z * Chunk::SIZE as i32; + + let sx = (self.x - cx).clamp(0, Chunk::SIZE as i32) as u32; + let sz = (self.z - cz).clamp(0, Chunk::SIZE as i32) as u32; + + let dx = (self.x + self.w - cx).clamp(0, Chunk::SIZE as i32) as u32; + let dz = (self.z + self.d - cz).clamp(0, Chunk::SIZE as i32) as u32; + + let sy = self.y.clamp(0, Chunk::HEIGHT as i32) as u32; + let dy = (self.y + self.h).clamp(0, Chunk::HEIGHT as i32) as u32; + + if sx < dx && sy < dy && sz < dz { + Some(InChunkBoundingBox { + x: sx, + y: sy, + z: sz, + w: dx - sx, + h: dy - sy, + d: dz - sz, + }) + } else { + None + } + } } impl World { pub fn new(block_registry: BlockRegistry) -> Self { Self { chunks: HashMap::new(), + pending_chunk_features: HashMap::new(), + unpopulated_chunks: HashSet::new(), block_registry, } } @@ -25,36 +100,169 @@ impl World { let mut chunk = Chunk::empty(); g.generate_terrain(coords, &mut chunk, &self.block_registry); + + // Populate chunk with pending features + if let Some(pending) = self.pending_chunk_features.remove(&coords) { + for pending in pending { + pending.place(coords, &mut chunk); + } + } + + self.unpopulated_chunks.insert(coords); self.chunks.insert(coords, chunk); } + fn populate_terrain(&mut self, coords: ChunkCoords, g: &mut G) { + if !self.chunks.contains_key(&coords) { + self.generate(coords, g); + } + } + + fn populate_terrain_at( + &mut self, + center: ChunkCoords, + radius: i32, + g: &mut G, + ) { + for cx in -radius..=radius { + for cz in -radius..=radius { + let coords = ChunkCoords::new(center.x + cx, center.z + cz); + self.populate_terrain(coords, g); + } + } + } + + fn place_feature(&mut self, feature: Arc) { + let aabb = feature.aabb(); + aabb.map_chunks(|coords| { + if let Some(chunk) = self.chunks.get_mut(&coords) { + feature.place(coords, chunk); + } else { + let pending = self.pending_chunk_features.entry(coords).or_default(); + pending.push(feature.clone()); + } + }); + } + + fn populate_features(&mut self) { + let mut features = vec![]; + + let log = self.block_registry.get("log").unwrap_or(0); + let leaves = self.block_registry.get("leaves").unwrap_or(0); + + for coords in self.unpopulated_chunks.drain() { + let Some(chunk) = self.chunks.get_mut(&coords) else { + continue; + }; + + // For each unpopulated chunk, generate a set of per-chunk features + let trees = rand::random_range(3..8); + + for _ in 0..trees { + // Generate trees + // TODO collision detection for these + let trunk_height = rand::random_range(5..8); + let leaves_radius = trunk_height / 2 + rand::random_range(0..2); + + let fx = rand::random_range(0..Chunk::SIZE as u32); + let fz = rand::random_range(0..Chunk::SIZE as u32); + let fy = chunk.top_height_at(fx, fz); + + let mut builder = FeatureBuilder::new(); + + for y in 0..trunk_height { + builder.set(0, y, 0, log); + } + for x in -leaves_radius..=leaves_radius { + for y in -leaves_radius..=leaves_radius { + for z in -leaves_radius..=leaves_radius { + let p = Vec3::new(x as f32, y as f32, z as f32); + if p.distance(Vec3::ZERO) > leaves_radius as f32 - 0.1 { + continue; + } + + builder.replace( + x, + y + trunk_height / 2 + leaves_radius / 2 + 1, + z, + 0, + leaves, + ); + } + } + } + + let (root_x, root_z) = coords.block_coords(fx, fz); + + features.push(Arc::new( + builder + .build(FeaturePlacementFilter::ReplaceAir) + .position_centered_horizontally(root_x, fy as i32, root_z), + )); + } + } + + // Place features + for feature in features { + self.place_feature(feature); + } + } + pub fn get(&self, coords: ChunkCoords) -> Option<&Chunk> { self.chunks.get(&coords) } + pub fn get_mut(&mut self, coords: ChunkCoords) -> Option<&mut Chunk> { + self.chunks.get_mut(&coords) + } + pub fn update_with_camera( &mut self, camera_position: Vec3, g: &mut G, ) { - const VIEW_DISTANCE: i32 = 5; + const VIEW_DISTANCE: i32 = 1; - fn cpos(x: f32) -> i32 { - (x as i32 + Chunk::SIZE as i32 / 2) / Chunk::SIZE as i32 - } + let camera_chunk = + ChunkCoords::from_block_coords(camera_position.x as i32, camera_position.z as i32); - let camera_cx = cpos(camera_position.x); - let camera_cz = cpos(camera_position.z); - - for cx in camera_cx - VIEW_DISTANCE..=camera_cx + VIEW_DISTANCE { - for cz in camera_cz - VIEW_DISTANCE..=camera_cz + VIEW_DISTANCE { - let coords = ChunkCoords::new(cx, cz); - - if !self.chunks.contains_key(&coords) { - self.generate(coords, g); - } - } - } + self.populate_terrain_at(camera_chunk, VIEW_DISTANCE, g); + self.populate_features(); + // self.populate_features_at(camera_chunk, VIEW_DISTANCE); + } +} + +#[cfg(test)] +mod tests { + use crate::world::ChunkCoords; + + use super::BoundingBox; + + #[test] + fn test_bounding_box_chunks() { + let aabb = BoundingBox { + x: -1, + y: 31, + z: 15, + w: 3, + h: 10, + d: 3, + }; + + let mut v0 = false; + let mut v1 = false; + + aabb.map_chunks(|coords| { + if coords == ChunkCoords::new(-1, 0) { + v0 = true; + } else if coords == ChunkCoords::new(0, 0) { + v1 = true; + } else { + panic!("Unexpected chunk coords: {coords:?}"); + } + }); + + assert!(v0 && v1); } } diff --git a/src/world/mod.rs b/src/world/mod.rs index 11dccba..ecbec0b 100644 --- a/src/world/mod.rs +++ b/src/world/mod.rs @@ -1,6 +1,7 @@ use bitflags::bitflags; pub mod chunk; +pub mod feature; pub mod generator; pub mod level;