trying to re-implement chunk_manager

This commit is contained in:
2026-03-19 21:52:58 +03:30
parent adebff1322
commit 45c122ad28
15 changed files with 216 additions and 566 deletions

View File

@@ -16,7 +16,7 @@ pub struct Chunk {
impl Chunk { impl Chunk {
/// pos_z and pos_x are `world` coordinates of the chunk, inside the world /// 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 { Chunk {
voxels: vec![0u32; BITPACKED_SIZE], voxels: vec![0u32; BITPACKED_SIZE],
world_position: (world_pos_x, world_pos_y, world_pos_z), world_position: (world_pos_x, world_pos_y, world_pos_z),
@@ -24,10 +24,14 @@ impl Chunk {
} }
} }
#[inline] // #[inline]
pub fn get_voxel_index(&self, local_pos: Vector3i, chunk_size: Vector3i) -> usize { // 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) // ((local_pos.x * chunk_size.y * chunk_size.z) + (local_pos.y * chunk_size.x) + local_pos.z)
as usize // 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] #[inline]
@@ -48,16 +52,15 @@ impl Chunk {
local_pos local_pos
); );
let index = self.get_voxel_index(local_pos, chunk_size); let index = Self::get_voxel_index(local_pos, chunk_size);
debug_assert!( debug_assert!(
index < ARR_SIZE, index < BITPACKED_SIZE,
"Voxel index {:?} out of chunk bounds", "Voxel index {:?} out of chunk bounds",
index index
); );
// Get the u32 containing this bit and the bit position within it let word_index = index >> 5; // divide by 32
let word_index = index / chunk_size.x as usize; let bit_index = index & 31; // modulo 32
let bit_index = index % chunk_size.x as usize;
(self.voxels[word_index] & (1 << bit_index)) != 0 (self.voxels[word_index] & (1 << bit_index)) != 0
} }
@@ -68,10 +71,10 @@ impl Chunk {
"Voxel position {:?} out of chunk bounds", "Voxel position {:?} out of chunk bounds",
voxel_pos 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 word_index = index >> 5;
let bit_index = index % chunk_size.x as usize; let bit_index = index & 31;
if is_solid { if is_solid {
self.voxels[word_index] |= 1 << bit_index; self.voxels[word_index] |= 1 << bit_index;
@@ -80,29 +83,4 @@ impl Chunk {
} }
self.modified = true; 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)
}
} }

View File

@@ -5,7 +5,8 @@ use godot::obj::Gd;
use crate::chunk::{Chunk, ChunkColumn, ChunkMesh}; use crate::chunk::{Chunk, ChunkColumn, ChunkMesh};
use crate::editor::voxel_registry::VoxelRegistry; 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::meshing::Mesher;
use crate::rendering::Renderer; use crate::rendering::Renderer;
use std::collections::HashMap; 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( pub fn update_around_player(
&mut self, &mut self,
player_world_pos: Vector3, player_world_pos: Vector3,
render_distance: i32, render_distance: i32,
registry: &VoxelRegistry, registry: &VoxelRegistry,
generator: &dyn WorldGenerator, surface_generator: &dyn SurfaceGenerator,
mesher: &dyn Mesher, mesher: &dyn Mesher,
) -> bool { ) -> bool {
let player_chunk_x = player_world_pos.x as i32 / self.chunk_size.x; let player_chunk_index_x = (player_world_pos.x as i32).div_euclid(self.chunk_size.x);
let player_chunk_z = player_world_pos.z as i32 / self.chunk_size.z; let player_chunk_index_z = (player_world_pos.z as i32).div_euclid(self.chunk_size.z);
let current_chunk = (player_chunk_x, player_chunk_z);
let current_chunk = (player_chunk_index_x, player_chunk_index_z);
// Early return if player is still in the same chunk // Early return if player is still in the same chunk
if current_chunk == self.last_player_chunk { if current_chunk == self.last_player_chunk {
@@ -67,20 +65,17 @@ impl ChunkManager {
self.pending_mesh_instances.clear(); self.pending_mesh_instances.clear();
// Unload distant chunks // Unload distant chunks
self.unload_distant_chunks(player_chunk_x, player_chunk_z, render_distance); self.update_loaded_chunks(
player_chunk_index_x,
let after_unload = now.elapsed().as_micros(); player_chunk_index_z,
// Load new chunks
self.load_chunks_around(
player_chunk_x,
player_chunk_z,
render_distance, render_distance,
registry, registry,
generator, surface_generator,
mesher, mesher,
); );
let after_unload = now.elapsed().as_micros();
godot_print!( godot_print!(
"Loading {}ms || Unloading {}us, ", "Loading {}ms || Unloading {}us, ",
(now.elapsed().as_micros() - after_unload) / 1000, (now.elapsed().as_micros() - after_unload) / 1000,
@@ -90,72 +85,58 @@ impl ChunkManager {
true true
} }
fn unload_distant_chunks(&mut self, center_x: i32, center_z: i32, distance: i32) { fn update_loaded_chunks(
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(
&mut self, &mut self,
center_x: i32, center_x: i32,
center_z: i32, center_z: i32,
distance: i32, distance: i32,
registry: &VoxelRegistry, registry: &VoxelRegistry,
generator: &dyn WorldGenerator, generator: &dyn SurfaceGenerator,
mesher: &dyn Mesher, 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 x in (center_x - distance)..=(center_x + distance) {
for z in (center_z - distance)..=(center_z + distance) { for z in (center_z - distance)..=(center_z + distance) {
if !self.chunk_columns.contains_key(&(x, z)) { desired.insert((x, z));
self.generate_chunk_column(x, z, registry, generator, mesher);
chunks_loaded += 1;
godot_print!("Loaded chunk column ({}, {})", x, z)
}
} }
} }
if chunks_loaded > 0 { // Unload
godot_print!("Loaded {} new chunk columns", chunks_loaded); 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( pub fn generate_chunk_column(
&mut self, &mut self,
x: i32, index_x: i32,
z: i32, index_z: i32,
registry: &VoxelRegistry, registry: &VoxelRegistry,
generator: &dyn WorldGenerator, generator: &dyn SurfaceGenerator,
mesher: &dyn Mesher, 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(); let start = Instant::now();
// Generate all chunks first generator.generate(&mut column);
for i in 0..self.terrain_height {
let chunk = column.get_or_create_chunk(i as i32);
generator.generate_chunk(chunk);
}
let generate_elapsed = start.elapsed().as_micros(); let generate_elapsed = start.elapsed().as_micros();
godot_print!("Generation took: {}μs", generate_elapsed); godot_print!("Generation took: {}μs", generate_elapsed);
@@ -168,7 +149,7 @@ impl ChunkManager {
} }
// Insert the column only after we're completely done with it // 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( fn mesh_and_render_chunk(
@@ -180,6 +161,7 @@ impl ChunkManager {
let mut mesh = ChunkMesh::new(); let mut mesh = ChunkMesh::new();
let start = Instant::now(); let start = Instant::now();
mesher.generate_mesh_with_registry(chunk, registry, &mut mesh); mesher.generate_mesh_with_registry(chunk, registry, &mut mesh);
if mesh.is_empty() { if mesh.is_empty() {
@@ -207,8 +189,8 @@ impl ChunkManager {
pub fn get_voxel_at(&self, world_pos: Vector3) -> bool { pub fn get_voxel_at(&self, world_pos: Vector3) -> bool {
// Convert world position to chunk coordinates // Convert world position to chunk coordinates
let chunk_x = (world_pos.x / 32.0).floor() as i32; let chunk_x = world_pos.x as i32 / self.chunk_size.x;
let chunk_z = (world_pos.z / 32.0).floor() as i32; let chunk_z = world_pos.z as i32 / self.chunk_size.z;
if let Some(column) = self.chunk_columns.get(&(chunk_x, chunk_z)) { if let Some(column) = self.chunk_columns.get(&(chunk_x, chunk_z)) {
// Pass the world position directly - let column handle the conversion // Pass the world position directly - let column handle the conversion

View File

@@ -5,8 +5,6 @@ use super::chunk::{CHUNK_SIZE, Chunk};
pub const CHUNKS_PER_COLUMN: usize = 8; pub const CHUNKS_PER_COLUMN: usize = 8;
pub struct ChunkColumn { pub struct ChunkColumn {
// from bottom to top
// TODO: replace with vec![]
pub chunks: [Option<Chunk>; CHUNKS_PER_COLUMN], pub chunks: [Option<Chunk>; CHUNKS_PER_COLUMN],
pub world_position: (i32, i32), // XZ pub world_position: (i32, i32), // XZ
pub chunk_size: Vector3i, pub chunk_size: Vector3i,
@@ -41,9 +39,8 @@ impl ChunkColumn {
let (world_x, world_z) = self.world_position; let (world_x, world_z) = self.world_position;
self.chunks[chunk_y_index as usize] = Some(Chunk::new( self.chunks[chunk_y_index as usize] = Some(Chunk::new(
world_x as f64, world_x as f64,
chunk_y_index as f64, (chunk_y_index) as f64,
world_z as f64, world_z as f64,
self.chunk_size,
)); ));
} }
self.chunks[chunk_y_index as usize].as_mut().unwrap() 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 { 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); let chunk_index = Self::get_chunk_index(self.chunk_size.y, world_pos.y);
if chunk_index < CHUNKS_PER_COLUMN { 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 chunk = self.get_or_create_chunk(chunk_index as i32);
let local_x = world_pos.x.rem_euclid(size.x); let local_x = world_pos.x.rem_euclid(size.x);

View File

@@ -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 { pub trait WorldGenerator: Send + Sync {
fn generate_chunk(&self, chunk: &mut Chunk); 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 should_be_solid(&self, density: f32, relative_height: f32) -> bool;
fn get_name(&self) -> &str; fn get_name(&self) -> &str;
} }
pub trait SurfaceGenerator: Send + Sync {
fn generate(&self, column: &mut ChunkColumn);
fn sample_voxel(&self, pos: Vector3i) -> bool;
}

View File

@@ -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"
}
}

View File

@@ -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,
}
}
}

View File

@@ -1,6 +1,5 @@
pub mod generator; pub mod generator;
pub mod heightmap; pub mod simple_heightmap;
pub mod layered_generator;
pub use generator::WorldGenerator; pub use generator::WorldGenerator;
pub use heightmap::HeightmapGenerator; pub use simple_heightmap::SimpleSurfaceGenerator;

View File

@@ -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
}
}

View File

@@ -69,7 +69,7 @@ impl BinaryGreedyMesher {
|| nz < 0 || nz < 0
|| nz >= size as i32 || nz >= size as i32
{ {
true // Outside chunk = air false // Outside chunk = air
} else { } else {
!chunk.get_voxel( !chunk.get_voxel(
Vector3i::new(nx, ny, nz), Vector3i::new(nx, ny, nz),

View File

@@ -128,7 +128,7 @@ impl CullingMesher {
let pos = Vector3::new(x as f32, y as f32, z as f32); let pos = Vector3::new(x as f32, y as f32, z as f32);
// Define the 4 vertices of the quad based on face direction // 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 // Add vertices
mesh.vertices.extend_from_slice(&[v0, v1, v2, v3]); mesh.vertices.extend_from_slice(&[v0, v1, v2, v3]);
@@ -153,8 +153,11 @@ impl CullingMesher {
base_index, base_index,
]); ]);
} }
fn get_face_vertices(
fn get_face_vertices(normal: Vector3, pos: Vector3) -> (Vector3, Vector3, Vector3, Vector3) { &self,
normal: Vector3,
pos: Vector3,
) -> (Vector3, Vector3, Vector3, Vector3) {
let half = Vector3::new(0.5, 0.5, 0.5); let half = Vector3::new(0.5, 0.5, 0.5);
let center = pos + half; let center = pos + half;

View File

@@ -165,194 +165,6 @@ impl TexturedMesher {
Vector2::new(u_min, v_min), // top-left 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 { impl Mesher for TexturedMesher {
@@ -368,11 +180,11 @@ impl Mesher for TexturedMesher {
) { ) {
mesh.clear(); mesh.clear();
let estimated_faces = 4096 / 4 * 3; // ~3000 faces const ESTIMATED_FACES: usize = (32 * 32 * 32) / 4 * 3; // ~24k faces
mesh.vertices.reserve(estimated_faces * 4); mesh.vertices.reserve(ESTIMATED_FACES * 4);
mesh.normals.reserve(estimated_faces * 4); mesh.normals.reserve(ESTIMATED_FACES * 4);
mesh.uvs.reserve(estimated_faces * 4); mesh.uvs.reserve(ESTIMATED_FACES * 4);
mesh.indices.reserve(estimated_faces * 6); mesh.indices.reserve(ESTIMATED_FACES * 6);
for x in 0..32 { for x in 0..32 {
for y in 0..32 { for y in 0..32 {

View File

@@ -8,6 +8,7 @@ use godot::{
geometry_instance_3d::ShadowCastingSetting, geometry_instance_3d::ShadowCastingSetting,
mesh::{ArrayType, PrimitiveType}, mesh::{ArrayType, PrimitiveType},
}, },
global::abs,
obj::IndexEnum, obj::IndexEnum,
prelude::*, prelude::*,
}; };
@@ -117,6 +118,12 @@ impl Renderer {
// You can also add some basic properties // You can also add some basic properties
material.set_roughness(0.8); material.set_roughness(0.8);
material.set_metallic(0.0); 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 // Create mesh instance
mesh_instance.set_material_override(&material); mesh_instance.set_material_override(&material);
@@ -195,6 +202,14 @@ impl Renderer {
// Apply material if available (do this without timing) // Apply material if available (do this without timing)
if let Some(mat) = &self.terrain_material { 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::<Material>();
mesh_instance.set_material_override(mat); mesh_instance.set_material_override(mat);
} }
@@ -205,8 +220,9 @@ impl Renderer {
} }
pub fn clear(&mut self) { pub fn clear(&mut self) {
for (_, instance) in self.mesh_instances.drain() { for (_, mut instance) in self.mesh_instances.drain() {
self.mesh_instance_pool.push(instance); // self.mesh_instance_pool.push(instance);
instance.queue_free();
} }
self.mesh_instances.clear(); self.mesh_instances.clear();
} }
@@ -219,8 +235,9 @@ impl Renderer {
} }
pub fn remove_chunk(&mut self, chunk_position: (i32, i32, i32)) { pub fn remove_chunk(&mut self, chunk_position: (i32, i32, i32)) {
if let Some(instance) = self.mesh_instances.remove(&chunk_position) { if let Some(mut instance) = self.mesh_instances.remove(&chunk_position) {
self.mesh_instance_pool.push(instance); instance.queue_free();
// self.mesh_instance_pool.push(instance);
} }
} }
} }

View File

@@ -3,4 +3,3 @@ pub mod registry;
pub mod voxel; pub mod voxel;
pub use model::*; pub use model::*;
pub use voxel::Voxel;

View File

@@ -3,7 +3,9 @@ use godot::prelude::*;
use crate::chunk::ChunkManager; use crate::chunk::ChunkManager;
use crate::editor::VoxelRegistry; 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}; use crate::meshing::{Mesher, TexturedMesher};
#[derive(GodotClass)] #[derive(GodotClass)]
@@ -43,7 +45,7 @@ pub struct World {
// World data (temporary) // World data (temporary)
chunk_manager: ChunkManager, chunk_manager: ChunkManager,
generator: Box<dyn WorldGenerator>, surface_generator: Box<dyn SurfaceGenerator>,
mesher: Box<dyn Mesher>, mesher: Box<dyn Mesher>,
} }
@@ -52,7 +54,17 @@ impl INode3D for World {
fn init(base: Base<Node3D>) -> Self { fn init(base: Base<Node3D>) -> Self {
godot_print!("🧊 Hello from FastVoxel"); 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 mesher = Box::new(TexturedMesher::new());
let chunk_manager = ChunkManager::new(); let chunk_manager = ChunkManager::new();
@@ -61,9 +73,9 @@ impl INode3D for World {
chunk_size: Vector3i::new(16, 16, 16), chunk_size: Vector3i::new(16, 16, 16),
world_seed: 1234, world_seed: 1234,
noise_frequency: 0.03, noise_frequency: 0.03,
terrain_height: 5, terrain_height: 8,
base, base,
generator, surface_generator,
mesher, mesher,
chunk_manager, chunk_manager,
workder_threads: 4, workder_threads: 4,
@@ -89,12 +101,13 @@ impl INode3D for World {
return; return;
} }
godot_print!("ChunkSize is {0}", self.chunk_size); let surface_generator = Box::new(SimpleSurfaceGenerator::new(
self.generator = Box::new(HeightmapGenerator::new( self.world_seed,
self.get_world_seed() as i64,
self.noise_frequency, 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.chunk_size = self.chunk_size;
self.chunk_manager.terrain_height = self.terrain_height; self.chunk_manager.terrain_height = self.terrain_height;
@@ -115,13 +128,13 @@ impl World {
let distance = self.render_distance as i32; let distance = self.render_distance as i32;
let library_ref = registry.bind(); let library_ref = registry.bind();
for x in -distance..=distance { for index_x in -distance..=distance {
for z in -distance..=distance { for index_z in -distance..=distance {
self.chunk_manager.generate_chunk_column( self.chunk_manager.generate_chunk_column(
x, index_x,
z, index_z,
&library_ref, &library_ref,
self.generator.as_ref(), self.surface_generator.as_ref(),
self.mesher.as_ref(), self.mesher.as_ref(),
); );
} }
@@ -163,7 +176,7 @@ impl World {
player_world_pos, player_world_pos,
self.render_distance as i32, self.render_distance as i32,
&registry_ref, &registry_ref,
self.generator.as_ref(), self.surface_generator.as_ref(),
self.mesher.as_ref(), self.mesher.as_ref(),
); );
} }

View File

@@ -0,0 +1,3 @@
pub struct WorldManager {
chunks: HashMap<(i32, i32), Chunk>,
}