diff --git a/src/chunk/chunk.rs b/src/chunk/chunk.rs index 4a5626f..ae3eff1 100644 --- a/src/chunk/chunk.rs +++ b/src/chunk/chunk.rs @@ -16,7 +16,7 @@ pub struct Chunk { impl Chunk { /// pos_z and pos_x are `world` coordinates of the chunk, inside the world - pub fn new(world_pos_x: f64, world_pos_y: f64, world_pos_z: f64, size: Vector3i) -> Self { + pub fn new(world_pos_x: f64, world_pos_y: f64, world_pos_z: f64) -> Self { Chunk { voxels: vec![0u32; BITPACKED_SIZE], world_position: (world_pos_x, world_pos_y, world_pos_z), @@ -24,10 +24,14 @@ impl Chunk { } } - #[inline] - pub fn get_voxel_index(&self, local_pos: Vector3i, chunk_size: Vector3i) -> usize { - ((local_pos.x * chunk_size.y * chunk_size.z) + (local_pos.y * chunk_size.x) + local_pos.z) - as usize + // #[inline] + // pub fn get_voxel_index(&self, local_pos: Vector3i, chunk_size: Vector3i) -> usize { + // ((local_pos.x * chunk_size.y * chunk_size.z) + (local_pos.y * chunk_size.x) + local_pos.z) + // as usize + // } + #[inline(always)] + fn get_voxel_index(local: Vector3i, chunk_size: Vector3i) -> usize { + ((local.x << 10) | (local.y << 5) | local.z) as usize } #[inline] @@ -48,16 +52,15 @@ impl Chunk { local_pos ); - let index = self.get_voxel_index(local_pos, chunk_size); + let index = Self::get_voxel_index(local_pos, chunk_size); debug_assert!( - index < ARR_SIZE, + index < BITPACKED_SIZE, "Voxel index {:?} out of chunk bounds", index ); - // Get the u32 containing this bit and the bit position within it - let word_index = index / chunk_size.x as usize; - let bit_index = index % chunk_size.x as usize; + let word_index = index >> 5; // divide by 32 + let bit_index = index & 31; // modulo 32 (self.voxels[word_index] & (1 << bit_index)) != 0 } @@ -68,10 +71,10 @@ impl Chunk { "Voxel position {:?} out of chunk bounds", voxel_pos ); - let index = self.get_voxel_index(voxel_pos, chunk_size); + let index = Self::get_voxel_index(voxel_pos, chunk_size); - let word_index = index / chunk_size.x as usize; - let bit_index = index % chunk_size.x as usize; + let word_index = index >> 5; + let bit_index = index & 31; if is_solid { self.voxels[word_index] |= 1 << bit_index; @@ -80,29 +83,4 @@ impl Chunk { } self.modified = true; } - - pub fn fill(&mut self, is_solid: bool) { - let fill_value = if is_solid { u32::MAX } else { 0 }; - self.voxels.fill(fill_value); - self.modified = true; - } - - pub fn get_position(&self) -> (f64, f64, f64) { - self.world_position - } - - pub fn get_world_bounds(&self, chunk_size: Vector3i) -> (Vector3, Vector3) { - let (x, y, z) = self.world_position; - let min = Vector3::new( - (x * chunk_size.x as f64) as f32, - (y * chunk_size.y as f64) as f32, - (z * chunk_size.z as f64) as f32, - ); - let max = Vector3::new( - ((x + 1.0) * chunk_size.x as f64) as f32, - ((y + 1.0) * chunk_size.y as f64) as f32, - ((z + 1.0) * chunk_size.z as f64) as f32, - ); - (min, max) - } } diff --git a/src/chunk/chunk_manager.rs b/src/chunk/chunk_manager.rs index 7d0d569..54f2f4d 100644 --- a/src/chunk/chunk_manager.rs +++ b/src/chunk/chunk_manager.rs @@ -5,7 +5,8 @@ use godot::obj::Gd; use crate::chunk::{Chunk, ChunkColumn, ChunkMesh}; use crate::editor::voxel_registry::VoxelRegistry; -use crate::generation::WorldGenerator; +use crate::generation::SimpleSurfaceGenerator; +use crate::generation::generator::SurfaceGenerator; use crate::meshing::Mesher; use crate::rendering::Renderer; use std::collections::HashMap; @@ -37,21 +38,18 @@ impl ChunkManager { } } - pub fn set_chunk_size(&mut self, chunk_size: Vector3i) { - self.chunk_size = chunk_size - } - pub fn update_around_player( &mut self, player_world_pos: Vector3, render_distance: i32, registry: &VoxelRegistry, - generator: &dyn WorldGenerator, + surface_generator: &dyn SurfaceGenerator, mesher: &dyn Mesher, ) -> bool { - let player_chunk_x = player_world_pos.x as i32 / self.chunk_size.x; - let player_chunk_z = player_world_pos.z as i32 / self.chunk_size.z; - let current_chunk = (player_chunk_x, player_chunk_z); + let player_chunk_index_x = (player_world_pos.x as i32).div_euclid(self.chunk_size.x); + let player_chunk_index_z = (player_world_pos.z as i32).div_euclid(self.chunk_size.z); + + let current_chunk = (player_chunk_index_x, player_chunk_index_z); // Early return if player is still in the same chunk if current_chunk == self.last_player_chunk { @@ -67,20 +65,17 @@ impl ChunkManager { self.pending_mesh_instances.clear(); // Unload distant chunks - self.unload_distant_chunks(player_chunk_x, player_chunk_z, render_distance); - - let after_unload = now.elapsed().as_micros(); - - // Load new chunks - self.load_chunks_around( - player_chunk_x, - player_chunk_z, + self.update_loaded_chunks( + player_chunk_index_x, + player_chunk_index_z, render_distance, registry, - generator, + surface_generator, mesher, ); + let after_unload = now.elapsed().as_micros(); + godot_print!( "Loading {}ms || Unloading {}us, ", (now.elapsed().as_micros() - after_unload) / 1000, @@ -90,72 +85,58 @@ impl ChunkManager { true } - fn unload_distant_chunks(&mut self, center_x: i32, center_z: i32, distance: i32) { - let mut to_remove = Vec::new(); - - for (&(x, z), _) in &self.chunk_columns { - let dx = (x - center_x).abs(); - let dz = (z - center_z).abs(); - - if dx > distance || dz > distance { - to_remove.push((x, z)); - } - } - - for (x, z) in to_remove { - if let Some(_) = self.chunk_columns.remove(&(x, z)) { - // Remove all chunk meshes in this column - for y in 0..8 { - self.renderer.remove_chunk((x, y, z)); - } - godot_print!("Unloaded chunk column ({}, {})", x, z); - } - } - } - - fn load_chunks_around( + fn update_loaded_chunks( &mut self, center_x: i32, center_z: i32, distance: i32, registry: &VoxelRegistry, - generator: &dyn WorldGenerator, + generator: &dyn SurfaceGenerator, mesher: &dyn Mesher, ) { - let mut chunks_loaded = 0; + use std::collections::HashSet; + + let mut desired = HashSet::new(); for x in (center_x - distance)..=(center_x + distance) { for z in (center_z - distance)..=(center_z + distance) { - if !self.chunk_columns.contains_key(&(x, z)) { - self.generate_chunk_column(x, z, registry, generator, mesher); - chunks_loaded += 1; - godot_print!("Loaded chunk column ({}, {})", x, z) - } + desired.insert((x, z)); } } - if chunks_loaded > 0 { - godot_print!("Loaded {} new chunk columns", chunks_loaded); + // Unload + self.chunk_columns.retain(|&(x, z), _| { + if desired.contains(&(x, z)) { + true + } else { + for y in 0..self.terrain_height { + self.renderer.remove_chunk((x, y as i32, z)); + } + false + } + }); + + // Load + for &(x, z) in &desired { + if !self.chunk_columns.contains_key(&(x, z)) { + self.generate_chunk_column(x, z, registry, generator, mesher); + } } } pub fn generate_chunk_column( &mut self, - x: i32, - z: i32, + index_x: i32, + index_z: i32, registry: &VoxelRegistry, - generator: &dyn WorldGenerator, + generator: &dyn SurfaceGenerator, mesher: &dyn Mesher, ) { - let mut column = ChunkColumn::new(x, z, self.chunk_size); + let mut column = ChunkColumn::new(index_x, index_z, self.chunk_size); let start = Instant::now(); - // Generate all chunks first - for i in 0..self.terrain_height { - let chunk = column.get_or_create_chunk(i as i32); - generator.generate_chunk(chunk); - } + generator.generate(&mut column); let generate_elapsed = start.elapsed().as_micros(); godot_print!("Generation took: {}μs", generate_elapsed); @@ -168,7 +149,7 @@ impl ChunkManager { } // Insert the column only after we're completely done with it - self.chunk_columns.insert((x, z), column); + self.chunk_columns.insert((index_x, index_z), column); } fn mesh_and_render_chunk( @@ -180,6 +161,7 @@ impl ChunkManager { let mut mesh = ChunkMesh::new(); let start = Instant::now(); + mesher.generate_mesh_with_registry(chunk, registry, &mut mesh); if mesh.is_empty() { @@ -207,8 +189,8 @@ impl ChunkManager { pub fn get_voxel_at(&self, world_pos: Vector3) -> bool { // Convert world position to chunk coordinates - let chunk_x = (world_pos.x / 32.0).floor() as i32; - let chunk_z = (world_pos.z / 32.0).floor() as i32; + let chunk_x = world_pos.x as i32 / self.chunk_size.x; + let chunk_z = world_pos.z as i32 / self.chunk_size.z; if let Some(column) = self.chunk_columns.get(&(chunk_x, chunk_z)) { // Pass the world position directly - let column handle the conversion diff --git a/src/chunk/column.rs b/src/chunk/column.rs index 1536cfa..862bed7 100644 --- a/src/chunk/column.rs +++ b/src/chunk/column.rs @@ -5,8 +5,6 @@ use super::chunk::{CHUNK_SIZE, Chunk}; pub const CHUNKS_PER_COLUMN: usize = 8; pub struct ChunkColumn { - // from bottom to top - // TODO: replace with vec![] pub chunks: [Option; CHUNKS_PER_COLUMN], pub world_position: (i32, i32), // XZ pub chunk_size: Vector3i, @@ -41,9 +39,8 @@ impl ChunkColumn { let (world_x, world_z) = self.world_position; self.chunks[chunk_y_index as usize] = Some(Chunk::new( world_x as f64, - chunk_y_index as f64, + (chunk_y_index) as f64, world_z as f64, - self.chunk_size, )); } self.chunks[chunk_y_index as usize].as_mut().unwrap() @@ -60,7 +57,7 @@ impl ChunkColumn { pub fn set_voxel(&mut self, world_pos: Vector3i, is_solid: bool) -> bool { let chunk_index = Self::get_chunk_index(self.chunk_size.y, world_pos.y); if chunk_index < CHUNKS_PER_COLUMN { - let size = self.chunk_size.clone(); + let size = self.chunk_size; let chunk = self.get_or_create_chunk(chunk_index as i32); let local_x = world_pos.x.rem_euclid(size.x); diff --git a/src/generation/generator.rs b/src/generation/generator.rs index c7aee1e..8a33d9b 100644 --- a/src/generation/generator.rs +++ b/src/generation/generator.rs @@ -1,4 +1,6 @@ -use crate::chunk::Chunk; +use godot::classes::class_macros::private::virtuals::Os::Vector3i; + +use crate::chunk::{Chunk, ChunkColumn, column}; pub trait WorldGenerator: Send + Sync { fn generate_chunk(&self, chunk: &mut Chunk); @@ -6,3 +8,8 @@ pub trait WorldGenerator: Send + Sync { fn should_be_solid(&self, density: f32, relative_height: f32) -> bool; fn get_name(&self) -> &str; } + +pub trait SurfaceGenerator: Send + Sync { + fn generate(&self, column: &mut ChunkColumn); + fn sample_voxel(&self, pos: Vector3i) -> bool; +} diff --git a/src/generation/heightmap.rs b/src/generation/heightmap.rs deleted file mode 100644 index d8d3ccd..0000000 --- a/src/generation/heightmap.rs +++ /dev/null @@ -1,200 +0,0 @@ -use super::generator::WorldGenerator; -use crate::chunk::Chunk; -use fastnoise_lite::*; -use godot::classes::class_macros::private::virtuals::Os::Vector3i; - -pub struct HeightmapGenerator { - noise: FastNoiseLite, - noise_detail: FastNoiseLite, - noise_caves: FastNoiseLite, - terrain_height: i32, - chunk_size: Vector3i, -} - -impl HeightmapGenerator { - pub fn new(seed: i64, frequency: f32, terrain_height: i32) -> Self { - // Main terrain noise - let mut noise = FastNoiseLite::new(); - noise.set_seed(Some(seed as i32)); - noise.set_frequency(Some(frequency * 0.5)); // Lower frequency for larger features - noise.set_noise_type(Some(fastnoise_lite::NoiseType::OpenSimplex2S)); - noise.set_fractal_type(Some(fastnoise_lite::FractalType::FBm)); - noise.set_fractal_octaves(Some(4)); - noise.set_fractal_lacunarity(Some(2.0)); - noise.set_fractal_gain(Some(0.5)); - - // Detail noise for small features - let mut noise_detail = FastNoiseLite::new(); - noise_detail.set_seed(Some(seed as i32 + 1)); - noise_detail.set_frequency(Some(frequency * 2.0)); - noise_detail.set_noise_type(Some(fastnoise_lite::NoiseType::OpenSimplex2S)); - - // Cave noise - let mut noise_caves = FastNoiseLite::new(); - noise_caves.set_seed(Some(seed as i32 + 2)); - noise_caves.set_frequency(Some(frequency * 1.5)); - noise_caves.set_noise_type(Some(fastnoise_lite::NoiseType::Perlin)); - - Self { - noise, - noise_detail, - noise_caves, - terrain_height, - chunk_size: Vector3i::new(32, 32, 32), - } - } -} - -impl WorldGenerator for HeightmapGenerator { - // fn generate_chunk(&self, chunk: &mut Chunk) { - // let (chunk_x, chunk_y, chunk_z) = chunk.world_position; - - // for local_x in 0..CHUNK_SIZE { - // for local_z in 0..CHUNK_SIZE { - // let world_x = chunk_x * CHUNK_SIZE as f64 + local_x as f64; - // let world_z = chunk_z * CHUNK_SIZE as f64 + local_z as f64; - - // let mut noise_value = self.noise.get_noise_2d(world_x as f32, world_z as f32); - // noise_value *= 2f32; - // let height = ((noise_value + 1.0) * 0.5 * self.terrain_height as f32) as i32; - - // for local_y in 0..=height.min(CHUNK_SIZE - 1) { - // let world_y = chunk_y as f64 * CHUNK_SIZE as f64 + local_y as f64; - - // let voxel = if world_y < height as f64 - 3.0 { - // Voxel::Stone - // } else if world_y < height as f64 - 1.0 { - // Voxel::Dirt - // } else if world_y <= height as f64 { - // Voxel::Grass - // } else if world_y < height as f64 + 2.0 { - // Voxel::Water - // } else { - // Voxel::Grass - // }; - // chunk.set_voxel( - // Vector3i::new(local_x, local_y, local_z), - // true, - // Vector3i { - // x: 32, - // y: 32, - // z: 32, - // }, - // ); - // } - // } - // } - // } - // - fn generate_chunk(&self, chunk: &mut Chunk) { - let (chunk_x, chunk_y, chunk_z) = chunk.world_position; - let chunk_world_y = chunk_y * self.chunk_size.y as f64; - - // Early exit: check if entire chunk is above or below terrain - let chunk_min_y = chunk_world_y as f32; - let chunk_max_y = (chunk_world_y + self.chunk_size.y as f64) as f32; - - // Pre-calculate all base heights for this chunk (32x32 = 1024 calculations instead of 32768) - let mut height_cache = [[0.0f32; 32]; 32]; - for local_x in 0..self.chunk_size.x { - let world_x = (chunk_x * self.chunk_size.x as f64 + local_x as f64) as f32; - for local_z in 0..self.chunk_size.z { - let world_z = (chunk_z * self.chunk_size.z as f64 + local_z as f64) as f32; - height_cache[local_x as usize][local_z as usize] = - self.get_base_height(world_x, world_z); - } - } - - // Check if we can fill entire chunk - let max_height = height_cache - .iter() - .flat_map(|row| row.iter()) - .cloned() - .fold(f32::MIN, f32::max); - if chunk_max_y < max_height - 10.0 { - // Entire chunk is underground - fill it - chunk.fill(true); - return; - } - - let min_height = height_cache - .iter() - .flat_map(|row| row.iter()) - .cloned() - .fold(f32::MAX, f32::min); - if chunk_min_y > min_height + 10.0 { - // Entire chunk is above terrain - leave as air - return; - } - - // Generate voxels with cached heights - for local_x in 0..self.chunk_size.x { - let world_x = (chunk_x * self.chunk_size.x as f64 + local_x as f64) as f32; - for local_z in 0..self.chunk_size.z { - let world_z = (chunk_z * self.chunk_size.z as f64 + local_z as f64) as f32; - let base_height = height_cache[local_x as usize][local_z as usize]; - - for local_y in 0..self.chunk_size.y { - let world_y = (chunk_world_y + local_y as f64) as f32; - let relative_height = world_y - base_height; - - // Use 3D noise for density only when needed - let is_solid = if relative_height < -3.0 { - // Deep underground - check for caves - let cave_noise = self.noise_caves.get_noise_3d(world_x, world_y, world_z); - cave_noise < 0.6 // Creates cave systems - } else if relative_height < 0.0 { - true // Just below surface - always solid - } else if relative_height < 15.0 { - let density_value = self.noise.get_noise_3d(world_x, world_y, world_z); - self.should_be_solid(density_value, relative_height) - } else { - false // Above terrain - }; - - if is_solid { - chunk.set_voxel( - Vector3i::new(local_x, local_y, local_z), - true, - self.chunk_size, - ); - } - } - } - } - } - - fn get_base_height(&self, x: f32, z: f32) -> f32 { - // Main terrain with multiple octaves (FBm already applied) - let height_value = self.noise.get_noise_2d(x, z); - - // Add detail noise for small features - let detail = self.noise_detail.get_noise_2d(x, z) * 0.15; - - // Combine and scale - creates hills and valleys like Minecraft - let combined = height_value + detail; - let height = (combined + 1.0) * 0.5 * self.terrain_height as f32; - - // Add some exponential scaling for more dramatic terrain - height.powf(1.3) - } - - fn should_be_solid(&self, density: f32, relative_height: f32) -> bool { - // Base terrain is solid below surface - if relative_height < 0.0 { - return true; - } - - // Create overhangs and floating islands - let cave_threshold = 0.35; // Higher = fewer caves - if relative_height < 15.0 && density > cave_threshold { - return true; - } - - false - } - - fn get_name(&self) -> &str { - "HeightmapGenerator" - } -} diff --git a/src/generation/layered_generator.rs b/src/generation/layered_generator.rs deleted file mode 100644 index 75e8c03..0000000 --- a/src/generation/layered_generator.rs +++ /dev/null @@ -1,39 +0,0 @@ -use fastnoise_lite::FastNoiseLite; - -// HIGHTLY EXPERIMENTAL!!! - -pub struct LayeredWorldGenerator { - base_noise: FastNoiseLite, // Large scale terrain - detail_noise: FastNoiseLite, // Medium details - cave_noise: FastNoiseLite, // Cave systems - biome_noise: FastNoiseLite, // Biome distribution - terrain_height: i32, -} - -impl LayeredWorldGenerator { - pub fn new(seed: i64) -> Self { - let mut base_noise = FastNoiseLite::new(); - base_noise.set_seed(Some(seed as i32)); - base_noise.set_frequency(Some(0.001)); // Very low frequency for continents - - let mut detail_noise = FastNoiseLite::new(); - detail_noise.set_seed(Some(seed as i32 + 1)); - detail_noise.set_frequency(Some(0.01)); // Medium frequency for hills - - let mut cave_noise = FastNoiseLite::new(); - cave_noise.set_seed(Some(seed as i32 + 2)); - cave_noise.set_frequency(Some(0.05)); // High frequency for small caves - - let mut biome_noise = FastNoiseLite::new(); - biome_noise.set_seed(Some(seed as i32 + 3)); - biome_noise.set_frequency(Some(0.0005)); // Very low for large biomes - - Self { - base_noise, - detail_noise, - cave_noise, - biome_noise, - terrain_height: 64, - } - } -} diff --git a/src/generation/mod.rs b/src/generation/mod.rs index 3a5d947..ffcfb21 100644 --- a/src/generation/mod.rs +++ b/src/generation/mod.rs @@ -1,6 +1,5 @@ pub mod generator; -pub mod heightmap; -pub mod layered_generator; +pub mod simple_heightmap; pub use generator::WorldGenerator; -pub use heightmap::HeightmapGenerator; +pub use simple_heightmap::SimpleSurfaceGenerator; diff --git a/src/generation/simple_heightmap.rs b/src/generation/simple_heightmap.rs new file mode 100644 index 0000000..4aab94f --- /dev/null +++ b/src/generation/simple_heightmap.rs @@ -0,0 +1,79 @@ +use fastnoise_lite::{FastNoiseLite, FractalType}; +use godot::{classes::class_macros::private::virtuals::Os::Vector3i, global::pow}; + +use crate::{chunk::ChunkColumn, generation::generator::SurfaceGenerator}; + +pub struct SimpleSurfaceGenerator { + noise: FastNoiseLite, + surface_height: u32, + chunk_size: Vector3i, +} + +impl SimpleSurfaceGenerator { + pub fn new(seed: i32, frequency: f32, surface_height: u32, chunk_size: Vector3i) -> Self { + let mut noise = FastNoiseLite::new(); + noise.frequency = frequency; + noise.noise_type = fastnoise_lite::NoiseType::OpenSimplex2; + noise.seed = seed; + noise.set_fractal_type(Some(FractalType::Ridged)); + noise.set_fractal_octaves(Some(4)); + Self { + noise, + surface_height, + chunk_size, + } + } + + #[inline] + pub fn normalize_value(value: f32) -> f32 { + (value + 1.0) * 0.5 // [0, value] + } +} + +impl SurfaceGenerator for SimpleSurfaceGenerator { + fn generate(&self, column: &mut ChunkColumn) { + let size_x = self.chunk_size.x; + let size_z = self.chunk_size.z; + + let base_x = column.world_position.0 * size_x; + let base_z = column.world_position.1 * size_z; + + let freq = 1.0 / 32.0; + + let column_height = self.chunk_size.y * self.surface_height as i32; + + for x in 0..self.chunk_size.x { + for z in 0..self.chunk_size.z { + // world coordinates of this column + let world_x = base_x + x; + let world_z = base_z + z; + + let noise = self + .noise + .get_noise_2d(world_x as f32 * freq, world_z as f32 * freq); + + let noise = Self::normalize_value(noise); + + let height = (noise.powi(2) * column_height as f32) as i32; + + for y in 1..=height { + column.set_voxel(Vector3i::new(world_x, y, world_z), true); + } + } + } + } + fn sample_voxel(&self, pos: Vector3i) -> bool { + let freq = 1.0 / 32.0; + + let noise = self + .noise + .get_noise_2d(pos.x as f32 * freq, pos.z as f32 * freq); + + let noise = Self::normalize_value(noise); + + let column_height = self.chunk_size.y * self.surface_height as i32; + let height = (noise.powi(2) * column_height as f32) as i32; + + pos.y <= height + } +} diff --git a/src/meshing/binary_greedy_mesher.rs b/src/meshing/binary_greedy_mesher.rs index 1b7094e..ef5e39c 100644 --- a/src/meshing/binary_greedy_mesher.rs +++ b/src/meshing/binary_greedy_mesher.rs @@ -69,7 +69,7 @@ impl BinaryGreedyMesher { || nz < 0 || nz >= size as i32 { - true // Outside chunk = air + false // Outside chunk = air } else { !chunk.get_voxel( Vector3i::new(nx, ny, nz), diff --git a/src/meshing/mesher.rs b/src/meshing/mesher.rs index 4f8d2aa..a18e9ca 100644 --- a/src/meshing/mesher.rs +++ b/src/meshing/mesher.rs @@ -128,7 +128,7 @@ impl CullingMesher { let pos = Vector3::new(x as f32, y as f32, z as f32); // Define the 4 vertices of the quad based on face direction - let (v0, v1, v2, v3) = Self::get_face_vertices(normal, pos); + let (v0, v1, v2, v3) = self.get_face_vertices(normal, pos); // Add vertices mesh.vertices.extend_from_slice(&[v0, v1, v2, v3]); @@ -153,8 +153,11 @@ impl CullingMesher { base_index, ]); } - - fn get_face_vertices(normal: Vector3, pos: Vector3) -> (Vector3, Vector3, Vector3, Vector3) { + fn get_face_vertices( + &self, + normal: Vector3, + pos: Vector3, + ) -> (Vector3, Vector3, Vector3, Vector3) { let half = Vector3::new(0.5, 0.5, 0.5); let center = pos + half; diff --git a/src/meshing/textured_mesher.rs b/src/meshing/textured_mesher.rs index a2fda2d..e553e95 100644 --- a/src/meshing/textured_mesher.rs +++ b/src/meshing/textured_mesher.rs @@ -165,194 +165,6 @@ impl TexturedMesher { Vector2::new(u_min, v_min), // top-left ] } - - fn add_voxel_to_mesh( - &self, - x: i32, - y: i32, - z: i32, - model_index: i32, - mesh: &mut ChunkMesh, - chunk: &Chunk, - registry: &VoxelRegistry, - ) { - if let Some(model_gd) = registry.models.get(model_index as usize) { - let model = model_gd.bind(); - - // Skip empty models - if model.is_empty() { - return; - } - - // Only handle cube models for now - if model.is_cube() { - self.add_cube_voxel_to_mesh(x, y, z, &model, mesh, chunk); - } else { - godot_print!("No model found for index: {}", model_index); - } - } - } - - fn add_cube_voxel_to_mesh( - &self, - x: i32, - y: i32, - z: i32, - model: &VoxelModel, - mesh: &mut ChunkMesh, - chunk: &Chunk, - ) { - let directions = [ - (Vector3i::new(1, 0, 0), Vector3::RIGHT, 1), // Right face, index 1 - (Vector3i::new(-1, 0, 0), Vector3::LEFT, 0), // Left face, index 0 - (Vector3i::new(0, 1, 0), Vector3::UP, 3), // Top face, index 3 - (Vector3i::new(0, -1, 0), Vector3::DOWN, 2), // Bottom face, index 2 - (Vector3i::new(0, 0, 1), Vector3::BACK, 4), // Back face, index 4 - (Vector3i::new(0, 0, -1), Vector3::FORWARD, 5), // Front face, index 5 - ]; - - for (offset, normal, face_index) in directions.iter() { - let neighbor_pos = Vector3i::new(x + offset.x, y + offset.y, z + offset.z); - - // If neighbor is out of bounds or not solid, create a face - if !chunk.is_voxel_within_bounds( - neighbor_pos, - Vector3i { - x: 32, - y: 32, - z: 32, - }, - ) || !chunk.get_voxel( - neighbor_pos, - Vector3i { - x: 32, - y: 32, - z: 32, - }, - ) { - self.add_textured_face(x, y, z, *normal, *face_index, model, mesh); - } - } - } - - fn add_textured_face( - &self, - x: i32, - y: i32, - z: i32, - normal: Vector3, - face_index: i32, - model: &VoxelModel, - mesh: &mut ChunkMesh, - ) { - let base_index = mesh.vertices.len() as i32; - let pos = Vector3::new(x as f32, y as f32, z as f32); - - // Get face vertices (same as before) - let (v0, v1, v2, v3) = self.get_face_vertices(normal, pos); - - // Add vertices - mesh.vertices.extend_from_slice(&[v0, v1, v2, v3]); - - // Add normals - let normal_vec = Vector3::new(normal.x as f32, normal.y as f32, normal.z as f32); - for _ in 0..4 { - mesh.normals.push(normal_vec); - } - - // Get texture coordinates for this face - let tile_coord = model.get_tile_for_face(face_index); - - // Get atlas size from the model - let atlas_size = Vector2i::new(16, 16); - - // Generate UV coordinates for this face - let uvs = self.generate_face_uvs(tile_coord, atlas_size); - mesh.uvs.extend_from_slice(&uvs); - - // Add indices (two triangles) - mesh.indices.extend_from_slice(&[ - base_index, - base_index + 1, - base_index + 2, - base_index + 2, - base_index + 3, - base_index, - ]); - } - - fn generate_face_uvs(&self, tile_coord: Vector2i, atlas_size: Vector2i) -> [Vector2; 4] { - let tile_size = Vector2::new(1.0 / atlas_size.x as f32, 1.0 / atlas_size.y as f32); - - let u_min = tile_coord.x as f32 * tile_size.x; - let u_max = (tile_coord.x + 1) as f32 * tile_size.x; - let v_min = tile_coord.y as f32 * tile_size.y; - let v_max = (tile_coord.y + 1) as f32 * tile_size.y; - - // Standard quad UV mapping - // Adjust the order based on your vertex winding - [ - Vector2::new(u_min, v_max), // bottom-left - Vector2::new(u_max, v_max), // bottom-right - Vector2::new(u_max, v_min), // top-right - Vector2::new(u_min, v_min), // top-left - ] - } - - fn get_face_vertices( - &self, - normal: Vector3, - pos: Vector3, - ) -> (Vector3, Vector3, Vector3, Vector3) { - let half = Vector3::new(0.5, 0.5, 0.5); - let center = pos + half; - - match (normal.x as i32, normal.y as i32, normal.z as i32) { - (1, 0, 0) => ( - // RIGHT - center + Vector3::new(0.5, -0.5, -0.5), - center + Vector3::new(0.5, -0.5, 0.5), - center + Vector3::new(0.5, 0.5, 0.5), - center + Vector3::new(0.5, 0.5, -0.5), - ), - (-1, 0, 0) => ( - // LEFT - center + Vector3::new(-0.5, -0.5, 0.5), - center + Vector3::new(-0.5, -0.5, -0.5), - center + Vector3::new(-0.5, 0.5, -0.5), - center + Vector3::new(-0.5, 0.5, 0.5), - ), - (0, 1, 0) => ( - // UP - center + Vector3::new(-0.5, 0.5, -0.5), - center + Vector3::new(0.5, 0.5, -0.5), - center + Vector3::new(0.5, 0.5, 0.5), - center + Vector3::new(-0.5, 0.5, 0.5), - ), - (0, -1, 0) => ( - // DOWN - center + Vector3::new(-0.5, -0.5, 0.5), - center + Vector3::new(0.5, -0.5, 0.5), - center + Vector3::new(0.5, -0.5, -0.5), - center + Vector3::new(-0.5, -0.5, -0.5), - ), - (0, 0, 1) => ( - // BACK - center + Vector3::new(0.5, -0.5, 0.5), - center + Vector3::new(-0.5, -0.5, 0.5), - center + Vector3::new(-0.5, 0.5, 0.5), - center + Vector3::new(0.5, 0.5, 0.5), - ), - (0, 0, -1) => ( - // FORWARD - center + Vector3::new(-0.5, -0.5, -0.5), - center + Vector3::new(0.5, -0.5, -0.5), - center + Vector3::new(0.5, 0.5, -0.5), - center + Vector3::new(-0.5, 0.5, -0.5), - ), - _ => panic!("wrong face"), // fallback, shouldn't happen - } - } } impl Mesher for TexturedMesher { @@ -368,11 +180,11 @@ impl Mesher for TexturedMesher { ) { mesh.clear(); - let estimated_faces = 4096 / 4 * 3; // ~3000 faces - mesh.vertices.reserve(estimated_faces * 4); - mesh.normals.reserve(estimated_faces * 4); - mesh.uvs.reserve(estimated_faces * 4); - mesh.indices.reserve(estimated_faces * 6); + const ESTIMATED_FACES: usize = (32 * 32 * 32) / 4 * 3; // ~24k faces + mesh.vertices.reserve(ESTIMATED_FACES * 4); + mesh.normals.reserve(ESTIMATED_FACES * 4); + mesh.uvs.reserve(ESTIMATED_FACES * 4); + mesh.indices.reserve(ESTIMATED_FACES * 6); for x in 0..32 { for y in 0..32 { diff --git a/src/rendering/renderer.rs b/src/rendering/renderer.rs index 5cad7fd..907357a 100644 --- a/src/rendering/renderer.rs +++ b/src/rendering/renderer.rs @@ -8,6 +8,7 @@ use godot::{ geometry_instance_3d::ShadowCastingSetting, mesh::{ArrayType, PrimitiveType}, }, + global::abs, obj::IndexEnum, prelude::*, }; @@ -117,6 +118,12 @@ impl Renderer { // You can also add some basic properties material.set_roughness(0.8); material.set_metallic(0.0); + material.set_albedo(Color { + r: (x as i32 % 2) as f32, + g: (y as i32 % 2) as f32, + b: (z as i32 % 2) as f32, + a: 255.0, + }); // Create mesh instance mesh_instance.set_material_override(&material); @@ -195,6 +202,14 @@ impl Renderer { // Apply material if available (do this without timing) if let Some(mat) = &self.terrain_material { + // let mut materi = StandardMaterial3D::new_gd(); + // materi.set_albedo(Color { + // r: (x as i32 % 2) as f32, + // g: (y as i32 % 2) as f32, + // b: (z as i32 % 2) as f32, + // a: 255.0, + // }); + // let ca = materi.upcast::(); mesh_instance.set_material_override(mat); } @@ -205,8 +220,9 @@ impl Renderer { } pub fn clear(&mut self) { - for (_, instance) in self.mesh_instances.drain() { - self.mesh_instance_pool.push(instance); + for (_, mut instance) in self.mesh_instances.drain() { + // self.mesh_instance_pool.push(instance); + instance.queue_free(); } self.mesh_instances.clear(); } @@ -219,8 +235,9 @@ impl Renderer { } pub fn remove_chunk(&mut self, chunk_position: (i32, i32, i32)) { - if let Some(instance) = self.mesh_instances.remove(&chunk_position) { - self.mesh_instance_pool.push(instance); + if let Some(mut instance) = self.mesh_instances.remove(&chunk_position) { + instance.queue_free(); + // self.mesh_instance_pool.push(instance); } } } diff --git a/src/voxel/mod.rs b/src/voxel/mod.rs index 4240759..6525b16 100644 --- a/src/voxel/mod.rs +++ b/src/voxel/mod.rs @@ -3,4 +3,3 @@ pub mod registry; pub mod voxel; pub use model::*; -pub use voxel::Voxel; diff --git a/src/world.rs b/src/world.rs index b9eedf0..131b633 100644 --- a/src/world.rs +++ b/src/world.rs @@ -3,7 +3,9 @@ use godot::prelude::*; use crate::chunk::ChunkManager; use crate::editor::VoxelRegistry; -use crate::generation::{HeightmapGenerator, WorldGenerator}; +use crate::generation::SimpleSurfaceGenerator; +use crate::generation::generator::SurfaceGenerator; +use crate::meshing::binary_greedy_mesher::BinaryGreedyMesher; use crate::meshing::{Mesher, TexturedMesher}; #[derive(GodotClass)] @@ -43,7 +45,7 @@ pub struct World { // World data (temporary) chunk_manager: ChunkManager, - generator: Box, + surface_generator: Box, mesher: Box, } @@ -52,7 +54,17 @@ impl INode3D for World { fn init(base: Base) -> Self { godot_print!("🧊 Hello from FastVoxel"); - let generator = Box::new(HeightmapGenerator::new(1234, 0.03, 5)); + let surface_generator = Box::new(SimpleSurfaceGenerator::new( + 1234, + 0.3, + 6, + Vector3i { + x: 32, + y: 32, + z: 32, + }, + )); + let mesher = Box::new(TexturedMesher::new()); let chunk_manager = ChunkManager::new(); @@ -61,9 +73,9 @@ impl INode3D for World { chunk_size: Vector3i::new(16, 16, 16), world_seed: 1234, noise_frequency: 0.03, - terrain_height: 5, + terrain_height: 8, base, - generator, + surface_generator, mesher, chunk_manager, workder_threads: 4, @@ -89,12 +101,13 @@ impl INode3D for World { return; } - godot_print!("ChunkSize is {0}", self.chunk_size); - self.generator = Box::new(HeightmapGenerator::new( - self.get_world_seed() as i64, + let surface_generator = Box::new(SimpleSurfaceGenerator::new( + self.world_seed, self.noise_frequency, - self.terrain_height as i32, + self.terrain_height, + self.chunk_size, )); + self.surface_generator = surface_generator; self.chunk_manager.chunk_size = self.chunk_size; self.chunk_manager.terrain_height = self.terrain_height; @@ -115,13 +128,13 @@ impl World { let distance = self.render_distance as i32; let library_ref = registry.bind(); - for x in -distance..=distance { - for z in -distance..=distance { + for index_x in -distance..=distance { + for index_z in -distance..=distance { self.chunk_manager.generate_chunk_column( - x, - z, + index_x, + index_z, &library_ref, - self.generator.as_ref(), + self.surface_generator.as_ref(), self.mesher.as_ref(), ); } @@ -163,7 +176,7 @@ impl World { player_world_pos, self.render_distance as i32, ®istry_ref, - self.generator.as_ref(), + self.surface_generator.as_ref(), self.mesher.as_ref(), ); } diff --git a/src/world/world_manager.rs b/src/world/world_manager.rs new file mode 100644 index 0000000..8ce3121 --- /dev/null +++ b/src/world/world_manager.rs @@ -0,0 +1,3 @@ +pub struct WorldManager { + chunks: HashMap<(i32, i32), Chunk>, +}