messy state

This commit is contained in:
2026-03-12 19:25:03 +03:30
commit a47f1f9818
30 changed files with 2791 additions and 0 deletions

108
src/chunk/chunk.rs Normal file
View File

@@ -0,0 +1,108 @@
use godot::prelude::*;
pub const CHUNK_SIZE: i32 = 32;
pub const ARR_SIZE: usize = CHUNK_SIZE.pow(3) as usize;
// 32x32x32 = 32768 bits, stored in u32s (32 bits each) = 1024 u32s
pub const BITPACKED_SIZE: usize = ARR_SIZE / 32;
#[derive(Debug)]
pub struct Chunk {
// Bitpacked voxel data: 0 = air, 1 = solid
pub voxels: Vec<u32>,
// WORLD COORDINATES
pub world_position: (f64, f64, f64),
modified: bool,
}
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 {
Chunk {
voxels: vec![0u32; BITPACKED_SIZE],
world_position: (world_pos_x, world_pos_y, world_pos_z),
modified: false,
}
}
#[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 is_voxel_within_bounds(&self, local_pos: Vector3i, chunk_size: Vector3i) -> bool {
local_pos.x >= 0
&& local_pos.x < chunk_size.x
&& local_pos.y >= 0
&& local_pos.y < chunk_size.y
&& local_pos.z >= 0
&& local_pos.z < chunk_size.z
}
#[inline]
pub fn get_voxel(&self, local_pos: Vector3i, chunk_size: Vector3i) -> bool {
debug_assert!(
self.is_voxel_within_bounds(local_pos, chunk_size),
"Voxel position {:?} out of chunk bounds",
local_pos
);
let index = self.get_voxel_index(local_pos, chunk_size);
debug_assert!(
index < ARR_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;
(self.voxels[word_index] & (1 << bit_index)) != 0
}
pub fn set_voxel(&mut self, voxel_pos: Vector3i, is_solid: bool, chunk_size: Vector3i) {
debug_assert!(
self.is_voxel_within_bounds(voxel_pos, chunk_size),
"Voxel position {:?} out of chunk bounds",
voxel_pos
);
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;
if is_solid {
self.voxels[word_index] |= 1 << bit_index;
} else {
self.voxels[word_index] &= !(1 << bit_index);
}
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)
}
}

229
src/chunk/chunk_manager.rs Normal file
View File

@@ -0,0 +1,229 @@
use godot::classes::MeshInstance3D;
use godot::classes::class_macros::private::virtuals::Os::{Vector3, Vector3i};
use godot::global::godot_print;
use godot::obj::Gd;
use crate::chunk::{Chunk, ChunkColumn, ChunkMesh};
use crate::editor::voxel_registry::VoxelRegistry;
use crate::generation::WorldGenerator;
use crate::meshing::Mesher;
use crate::rendering::Renderer;
use std::collections::HashMap;
use std::time::Instant;
pub struct ChunkManager {
pub chunk_columns: HashMap<(i32, i32), ChunkColumn>,
pub renderer: Renderer,
pub last_player_chunk: (i32, i32),
// terrain parameteres
pub chunk_size: Vector3i,
pub terrain_height: u32,
// Track which mesh instances need to be added to scene inefficient as but who cares
// the mesher is efficient enough
pub pending_mesh_instances: Vec<Gd<MeshInstance3D>>,
}
impl ChunkManager {
pub fn new() -> Self {
Self {
chunk_columns: HashMap::new(),
renderer: Renderer::new(),
last_player_chunk: (0, 0),
chunk_size: Vector3i::ZERO,
terrain_height: 0,
pending_mesh_instances: Vec::new(),
}
}
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,
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);
// Early return if player is still in the same chunk
if current_chunk == self.last_player_chunk {
return false;
}
let now = Instant::now();
godot_print!("Player moved to chunk: {:?}", current_chunk);
self.last_player_chunk = current_chunk;
// Clear pending instances from previous updates
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,
render_distance,
registry,
generator,
mesher,
);
godot_print!(
"Loading {}ms || Unloading {}us, ",
(now.elapsed().as_micros() - after_unload) / 1000,
after_unload
);
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(
&mut self,
center_x: i32,
center_z: i32,
distance: i32,
registry: &VoxelRegistry,
generator: &dyn WorldGenerator,
mesher: &dyn Mesher,
) {
let mut chunks_loaded = 0;
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)
}
}
}
if chunks_loaded > 0 {
godot_print!("Loaded {} new chunk columns", chunks_loaded);
}
}
pub fn generate_chunk_column(
&mut self,
x: i32,
z: i32,
registry: &VoxelRegistry,
generator: &dyn WorldGenerator,
mesher: &dyn Mesher,
) {
let mut column = ChunkColumn::new(x, 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);
}
let generate_elapsed = start.elapsed().as_micros();
godot_print!("Generation took: {}μs", generate_elapsed);
// Mesh and render each chunk
for i in 0..self.terrain_height {
if let Some(chunk) = column.get_chunk(i as usize) {
self.mesh_and_render_chunk(chunk, mesher, registry);
}
}
// Insert the column only after we're completely done with it
self.chunk_columns.insert((x, z), column);
}
fn mesh_and_render_chunk(
&mut self,
chunk: &Chunk,
mesher: &dyn Mesher,
registry: &VoxelRegistry,
) {
let mut mesh = ChunkMesh::new();
let start = Instant::now();
mesher.generate_mesh_with_registry(chunk, registry, &mut mesh);
if mesh.is_empty() {
// Silent - empty chunks are normal
return;
}
godot_print!("Meshing Took: {}μs", start.elapsed().as_micros());
let start = Instant::now();
// Use render_chunk (with colors) instead of render_chunk_with_uvs
let mesh_instance = self.renderer.render_chunk(chunk, &mesh);
godot_print!("Rendering Took: {}μs", start.elapsed().as_micros());
self.pending_mesh_instances.push(mesh_instance);
}
pub fn clear(&mut self) {
// Remove all mesh instances from renderer
self.renderer.clear();
self.chunk_columns.clear();
self.pending_mesh_instances.clear();
// self.last_player_chunk = (0, 0);
}
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;
if let Some(column) = self.chunk_columns.get(&(chunk_x, chunk_z)) {
// Pass the world position directly - let column handle the conversion
column.get_voxel(Vector3i::new(
world_pos.x as i32, // Use actual world coordinates
world_pos.y as i32,
world_pos.z as i32,
))
} else {
false // Air by default
}
}
// Get pending mesh instances and clear the list
pub fn take_pending_mesh_instances(&mut self) -> Vec<Gd<MeshInstance3D>> {
std::mem::take(&mut self.pending_mesh_instances)
}
}

89
src/chunk/column.rs Normal file
View File

@@ -0,0 +1,89 @@
use godot::classes::class_macros::private::virtuals::Os::Vector3i;
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<Chunk>; CHUNKS_PER_COLUMN],
pub world_position: (i32, i32), // XZ
pub chunk_size: Vector3i,
}
impl ChunkColumn {
pub fn new(x: i32, z: i32, chunk_size: Vector3i) -> Self {
ChunkColumn {
chunks: [None, None, None, None, None, None, None, None],
world_position: (x, z),
chunk_size,
}
}
#[inline]
pub fn get_chunk_index(chunk_size_y: i32, world_y: i32) -> usize {
(world_y / chunk_size_y) as usize
}
#[inline]
pub fn get_local_y(chunk_size_y: i32, world_y: i32) -> i32 {
world_y % chunk_size_y
}
#[inline]
pub fn get_world_y(chunk_size_y: i32, chunk_index: i32, local_y: i32) -> i32 {
(chunk_index * chunk_size_y) + local_y
}
pub fn get_or_create_chunk(&mut self, chunk_y_index: i32) -> &mut Chunk {
if self.chunks[chunk_y_index as usize].is_none() {
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,
world_z as f64,
self.chunk_size,
));
}
self.chunks[chunk_y_index as usize].as_mut().unwrap()
}
pub fn get_chunk(&self, chunk_y_index: usize) -> Option<&Chunk> {
self.chunks[chunk_y_index].as_ref()
}
pub fn get_chunk_mut(&mut self, chunk_y_index: usize) -> Option<&mut Chunk> {
self.chunks[chunk_y_index].as_mut()
}
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 chunk = self.get_or_create_chunk(chunk_index as i32);
let local_x = world_pos.x.rem_euclid(size.x);
let local_y = Self::get_local_y(size.y, world_pos.y);
let local_z = world_pos.z.rem_euclid(size.z);
chunk.set_voxel(Vector3i::new(local_x, local_y, local_z), is_solid, size);
true
} else {
false
}
}
pub fn get_voxel(&self, world_pos: Vector3i) -> bool {
let chunk_index = Self::get_chunk_index(self.chunk_size.y, world_pos.y);
if let Some(chunk) = self.get_chunk(chunk_index) {
let local_x = world_pos.x.rem_euclid(CHUNK_SIZE);
let local_y = Self::get_local_y(self.chunk_size.y, world_pos.y);
let local_z = world_pos.z.rem_euclid(CHUNK_SIZE);
chunk.get_voxel(Vector3i::new(local_x, local_y, local_z), self.chunk_size)
} else {
false // Air by default
}
}
}

40
src/chunk/mesh.rs Normal file
View File

@@ -0,0 +1,40 @@
use godot::prelude::*;
#[derive(Debug)]
pub struct ChunkMesh {
pub vertices: Vec<Vector3>,
pub normals: Vec<Vector3>,
pub colors: Vec<Color>,
pub uvs: Vec<Vector2>,
pub indices: Vec<i32>,
}
impl Default for ChunkMesh {
fn default() -> Self {
Self::new()
}
}
impl ChunkMesh {
pub fn new() -> Self {
Self {
vertices: Vec::new(),
normals: Vec::new(),
colors: Vec::new(),
uvs: Vec::new(),
indices: Vec::new(),
}
}
pub fn clear(&mut self) {
self.vertices.clear();
self.normals.clear();
self.colors.clear();
self.uvs.clear();
self.indices.clear();
}
pub fn is_empty(&self) -> bool {
self.vertices.is_empty()
}
}

9
src/chunk/mod.rs Normal file
View File

@@ -0,0 +1,9 @@
pub mod chunk;
pub mod chunk_manager;
pub mod column;
pub mod mesh;
pub use chunk::Chunk;
pub use chunk_manager::ChunkManager;
pub use column::ChunkColumn;
pub use mesh::ChunkMesh;

3
src/editor/mod.rs Normal file
View File

@@ -0,0 +1,3 @@
pub mod voxel_registry;
pub use voxel_registry::VoxelRegistry;

View File

@@ -0,0 +1,95 @@
use godot::{
classes::{
Resource,
class_macros::private::virtuals::Os::{Array, Vector2i},
},
obj::{Base, Gd},
prelude::{Export, GodotClass, GodotConvert, Var},
};
#[derive(GodotConvert, Var, Export, Debug, Clone, Copy)]
#[godot(via = i64)]
pub enum VoxelType {
Empty,
Cube,
Mesh,
Fluid,
}
impl Default for VoxelType {
fn default() -> Self {
Self::Empty
}
}
#[derive(GodotClass, Debug)]
#[class(init, base=Resource, tool)]
pub struct VoxelRegistry {
#[export]
pub models: Array<Gd<VoxelModel>>,
base: Base<Resource>,
}
#[derive(GodotClass, Debug, Clone, Copy)]
#[class(init, base=Resource, tool)]
pub struct VoxelModel {
#[export]
voxel_type: VoxelType,
#[export]
atlas_size_in_tiles: Vector2i,
#[export_group(name = "Cube Tiles")]
#[export]
tile_left: Vector2i,
#[export]
tile_right: Vector2i,
#[export]
tile_bottom: Vector2i,
#[export]
tile_top: Vector2i,
#[export]
tile_back: Vector2i,
#[export]
tile_front: Vector2i,
}
impl VoxelModel {
#[inline]
pub fn is_solid(&self) -> bool {
match self.voxel_type {
VoxelType::Empty => false,
_ => true,
}
}
#[inline]
pub fn is_empty(&self) -> bool {
match self.voxel_type {
VoxelType::Empty => true,
_ => false,
}
}
#[inline]
pub fn is_cube(&self) -> bool {
match self.voxel_type {
VoxelType::Cube => true,
_ => false,
}
}
#[inline]
pub fn get_tile_for_face(&self, face: i32) -> Vector2i {
match face {
0 => self.tile_left, // Left
1 => self.tile_right, // Right
2 => self.tile_bottom, // Bottom
3 => self.tile_top, // Top
4 => self.tile_back, // Back
5 => self.tile_front, // Front
_ => Vector2i::new(0, 0),
}
}
}

View File

@@ -0,0 +1,8 @@
use crate::chunk::Chunk;
pub trait WorldGenerator: Send + Sync {
fn generate_chunk(&self, chunk: &mut Chunk);
fn get_base_height(&self, x: f32, z: f32) -> f32;
fn should_be_solid(&self, density: f32, relative_height: f32) -> bool;
fn get_name(&self) -> &str;
}

202
src/generation/heightmap.rs Normal file
View File

@@ -0,0 +1,202 @@
use super::generator::WorldGenerator;
use crate::chunk::Chunk;
use crate::chunk::chunk::CHUNK_SIZE;
use crate::voxel::Voxel;
use fastnoise_lite::*;
use godot::classes::class_macros::private::virtuals::Os::{Vector2i, 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

@@ -0,0 +1,39 @@
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,
}
}
}

7
src/generation/mod.rs Normal file
View File

@@ -0,0 +1,7 @@
pub mod generator;
pub mod heightmap;
pub mod layered_generator;
pub use generator::WorldGenerator;
pub use heightmap::HeightmapGenerator;
pub use layered_generator::LayeredWorldGenerator;

15
src/lib.rs Normal file
View File

@@ -0,0 +1,15 @@
use godot::prelude::*;
mod voxel;
mod world;
mod chunk;
mod editor;
mod generation;
mod meshing;
mod rendering;
struct FastVoxel;
#[gdextension]
unsafe impl ExtensionLibrary for FastVoxel {}

View File

@@ -0,0 +1,317 @@
use crate::chunk::chunk::CHUNK_SIZE;
use crate::chunk::{Chunk, ChunkMesh};
use crate::editor::voxel_registry::VoxelRegistry;
use crate::meshing::Mesher;
use godot::prelude::*;
#[derive(Debug, Clone, Copy)]
pub struct BinaryGreedyMesher;
#[derive(Debug, Clone, Copy)]
struct GreedyQuad {
x: u32,
y: u32,
w: u32,
h: u32,
}
impl BinaryGreedyMesher {
pub fn new() -> Self {
Self
}
pub fn generate_mesh(&self, chunk: &Chunk, mesh: &mut ChunkMesh) {
mesh.vertices.clear();
mesh.normals.clear();
mesh.indices.clear();
// Generate mesh for each of the 6 faces
self.mesh_face(chunk, mesh, 0); // -X (left)
self.mesh_face(chunk, mesh, 1); // +X (right)
self.mesh_face(chunk, mesh, 2); // -Y (down)
self.mesh_face(chunk, mesh, 3); // +Y (up)
self.mesh_face(chunk, mesh, 4); // -Z (back)
self.mesh_face(chunk, mesh, 5); // +Z (forward)
}
fn mesh_face(&self, chunk: &Chunk, mesh: &mut ChunkMesh, face_dir: usize) {
let size = CHUNK_SIZE as usize;
// For each slice perpendicular to the face direction
for axis in 0..size {
// Generate binary mask for this slice
let mut face_mask = [0u32; 32];
for row in 0..size {
for col in 0..size {
let (x, y, z) = self.get_coords_for_face(face_dir, axis, row, col);
if x >= size || y >= size || z >= size {
continue;
}
let pos = Vector3i::new(x as i32, y as i32, z as i32);
let is_solid = chunk.get_voxel(
pos,
Vector3i {
x: 32,
y: 32,
z: 32,
},
);
// Check if neighbor in face direction is air
let (nx, ny, nz) = self.get_neighbor_coords(face_dir, x, y, z);
let neighbor_is_air = if nx < 0
|| nx >= size as i32
|| ny < 0
|| ny >= size as i32
|| nz < 0
|| nz >= size as i32
{
true // Outside chunk = air
} else {
!chunk.get_voxel(
Vector3i::new(nx, ny, nz),
Vector3i {
x: 32,
y: 32,
z: 32,
},
)
};
// Face is visible if this voxel is solid and neighbor is air
if is_solid && neighbor_is_air {
face_mask[row] |= 1 << col;
}
}
}
// Greedy mesh the binary mask
let quads = self.greedy_mesh_binary_plane(face_mask);
// Add quads to mesh
for quad in quads {
self.add_quad_to_mesh(mesh, quad, face_dir, axis, chunk);
}
}
}
#[inline]
fn get_coords_for_face(
&self,
face_dir: usize,
axis: usize,
row: usize,
col: usize,
) -> (usize, usize, usize) {
match face_dir {
0 => (axis, col, row), // -X: axis=x, row=z, col=y
1 => (axis, col, row), // +X: axis=x, row=z, col=y
2 => (row, axis, col), // -Y: axis=y, row=x, col=z
3 => (row, axis, col), // +Y: axis=y, row=x, col=z
4 => (row, col, axis), // -Z: axis=z, row=x, col=y
5 => (row, col, axis), // +Z: axis=z, row=x, col=y
_ => unreachable!(),
}
}
#[inline]
fn get_neighbor_coords(
&self,
face_dir: usize,
x: usize,
y: usize,
z: usize,
) -> (i32, i32, i32) {
let (x, y, z) = (x as i32, y as i32, z as i32);
match face_dir {
0 => (x - 1, y, z), // -X
1 => (x + 1, y, z), // +X
2 => (x, y - 1, z), // -Y
3 => (x, y + 1, z), // +Y
4 => (x, y, z - 1), // -Z
5 => (x, y, z + 1), // +Z
_ => unreachable!(),
}
}
fn greedy_mesh_binary_plane(&self, mut data: [u32; 32]) -> Vec<GreedyQuad> {
let mut quads = Vec::new();
let size = CHUNK_SIZE as u32;
for row in 0..data.len() {
let mut y = 0;
while y < size {
// Find first solid bit (skip zeros)
y += (data[row] >> y).trailing_zeros();
if y >= size {
break; // Reached end of row
}
// Count consecutive solid bits (height)
let h = (data[row] >> y).trailing_ones();
// Create a mask for these bits
let h_as_mask = if h >= 32 { u32::MAX } else { (1u32 << h) - 1 };
let mask = h_as_mask << y;
// Try to expand horizontally
let mut w = 1;
while row + w < size as usize {
// Check if the next row has the same pattern at this position
let next_row_bits = (data[row + w] >> y) & h_as_mask;
if next_row_bits != h_as_mask {
break; // Can't expand further
}
// Clear the bits we're consuming
data[row + w] &= !mask;
w += 1;
}
quads.push(GreedyQuad {
x: row as u32,
y,
w: w as u32,
h,
});
y += h;
}
}
quads
}
fn add_quad_to_mesh(
&self,
mesh: &mut ChunkMesh,
quad: GreedyQuad,
face_dir: usize,
axis: usize,
chunk: &Chunk,
) {
let base_idx = mesh.vertices.len() as i32;
// Convert quad coordinates back to 3D positions
let vertices = self.get_quad_vertices(quad, face_dir, axis);
let normal = self.get_face_normal(face_dir);
// Base color based on face direction with simple directional shading (no per-vertex AO)
let (r, g, b) = match face_dir {
0 => (0.55, 0.55, 0.55), // -X (left) - darker gray
1 => (0.65, 0.65, 0.65), // +X (right) - lighter gray
2 => (0.35, 0.35, 0.35), // -Y (down) - dark gray
3 => (0.5, 0.8, 0.3), // +Y (up) - grass green (brightest)
4 => (0.50, 0.50, 0.50), // -Z (back) - medium gray
5 => (0.60, 0.60, 0.60), // +Z (forward) - lighter gray
_ => (1.0, 1.0, 1.0),
};
let color = Color::from_rgb(r, g, b);
// Add vertices with consistent color (no per-vertex variation)
for vertex in &vertices {
mesh.vertices.push(*vertex);
mesh.normals.push(normal);
mesh.colors.push(color);
}
// Add indices for two triangles
mesh.indices.push(base_idx);
mesh.indices.push(base_idx + 1);
mesh.indices.push(base_idx + 2);
mesh.indices.push(base_idx);
mesh.indices.push(base_idx + 2);
mesh.indices.push(base_idx + 3);
}
fn get_quad_vertices(&self, quad: GreedyQuad, face_dir: usize, axis: usize) -> [Vector3; 4] {
let x = quad.x as f32;
let y = quad.y as f32;
let w = quad.w as f32;
let h = quad.h as f32;
let a = axis as f32;
match face_dir {
0 => [
// -X (left)
Vector3::new(a, y, x),
Vector3::new(a, y + h, x),
Vector3::new(a, y + h, x + w),
Vector3::new(a, y, x + w),
],
1 => [
// +X (right)
Vector3::new(a + 1.0, y, x),
Vector3::new(a + 1.0, y, x + w),
Vector3::new(a + 1.0, y + h, x + w),
Vector3::new(a + 1.0, y + h, x),
],
2 => [
// -Y (down)
Vector3::new(x, a, y),
Vector3::new(x, a, y + h),
Vector3::new(x + w, a, y + h),
Vector3::new(x + w, a, y),
],
3 => [
// +Y (up)
Vector3::new(x, a + 1.0, y),
Vector3::new(x + w, a + 1.0, y),
Vector3::new(x + w, a + 1.0, y + h),
Vector3::new(x, a + 1.0, y + h),
],
4 => [
// -Z (back)
Vector3::new(x, y, a),
Vector3::new(x + w, y, a),
Vector3::new(x + w, y + h, a),
Vector3::new(x, y + h, a),
],
5 => [
// +Z (forward)
Vector3::new(x, y, a + 1.0),
Vector3::new(x, y + h, a + 1.0),
Vector3::new(x + w, y + h, a + 1.0),
Vector3::new(x + w, y, a + 1.0),
],
_ => unreachable!(),
}
}
fn get_face_normal(&self, face_dir: usize) -> Vector3 {
match face_dir {
0 => Vector3::new(-1.0, 0.0, 0.0), // -X
1 => Vector3::new(1.0, 0.0, 0.0), // +X
2 => Vector3::new(0.0, -1.0, 0.0), // -Y
3 => Vector3::new(0.0, 1.0, 0.0), // +Y
4 => Vector3::new(0.0, 0.0, -1.0), // -Z
5 => Vector3::new(0.0, 0.0, 1.0), // +Z
_ => unreachable!(),
}
}
}
impl Mesher for BinaryGreedyMesher {
fn generate_mesh(&self, chunk: &Chunk, mesh: &mut ChunkMesh) {
self.generate_mesh(chunk, mesh);
}
fn generate_mesh_with_registry(
&self,
chunk: &Chunk,
_registry: &VoxelRegistry,
mesh: &mut ChunkMesh,
) {
// Binary greedy mesher doesn't use registry (only solid/air)
self.generate_mesh(chunk, mesh);
}
fn get_name(&self) -> &str {
"BinaryGreedyMesher"
}
}

207
src/meshing/mesher.rs Normal file
View File

@@ -0,0 +1,207 @@
use crate::chunk::{Chunk, ChunkMesh};
use crate::editor::voxel_registry::VoxelRegistry;
use godot::prelude::*;
pub trait Mesher: Send + Sync {
fn generate_mesh(&self, chunk: &Chunk, mesh: &mut ChunkMesh);
fn generate_mesh_with_registry(
&self,
chunk: &Chunk,
registry: &VoxelRegistry,
mesh: &mut ChunkMesh,
);
fn get_name(&self) -> &str;
}
pub struct CullingMesher;
impl CullingMesher {
pub fn new() -> Self {
Self
}
}
impl Mesher for CullingMesher {
fn generate_mesh_with_registry(
&self,
chunk: &Chunk,
registry: &VoxelRegistry,
mesh: &mut ChunkMesh,
) {
// panic!("Not implemented for CullingMesher")
self.generate_mesh(chunk, mesh);
}
fn generate_mesh(&self, chunk: &Chunk, mesh: &mut ChunkMesh) {
mesh.clear();
// Simple CullingMesher - generate a quad for each solid voxel face
// This is very basic and should be optimized later with greedy meshing
for x in 0..16 {
for y in 0..16 {
for z in 0..16 {
let voxel = chunk.get_voxel(
Vector3i::new(x, y, z),
Vector3i {
x: 32,
y: 32,
z: 32,
},
);
if !voxel {
continue;
}
self.add_voxel_to_mesh(x, y, z, voxel, mesh, chunk);
}
}
}
}
fn get_name(&self) -> &str {
"CullingMesher"
}
}
impl CullingMesher {
fn add_voxel_to_mesh(
&self,
x: i32,
y: i32,
z: i32,
voxel: bool,
mesh: &mut ChunkMesh,
chunk: &Chunk,
) {
let color = Color {
r: 255.0,
g: 255.0,
b: 255.0,
a: 255.0,
};
// Check each face and add it if the neighbor is air
let directions = [
(Vector3i::new(1, 0, 0), Vector3::RIGHT),
(Vector3i::new(-1, 0, 0), Vector3::LEFT),
(Vector3i::new(0, 1, 0), Vector3::UP),
(Vector3i::new(0, -1, 0), Vector3::DOWN),
(Vector3i::new(0, 0, 1), Vector3::BACK),
(Vector3i::new(0, 0, -1), Vector3::FORWARD),
];
for (offset, normal) in directions.iter() {
let neighbor_pos = Vector3i::new(x + offset.x, y + offset.y, z + offset.z);
// If neighbor is out of bounds or air, 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_face(x, y, z, *normal, color, mesh);
}
}
}
fn add_face(
&self,
x: i32,
y: i32,
z: i32,
normal: Vector3,
color: Color,
mesh: &mut ChunkMesh,
) {
let base_index = mesh.vertices.len() as i32;
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);
// Add vertices
mesh.vertices.extend_from_slice(&[v0, v1, v2, v3]);
// Add normals (all same for the face)
for _ in 0..4 {
mesh.normals.push(normal);
}
// Add colors
for _ in 0..4 {
mesh.colors.push(color);
}
// 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 get_face_vertices(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
}
}
}

7
src/meshing/mod.rs Normal file
View File

@@ -0,0 +1,7 @@
pub mod binary_greedy_mesher;
pub mod mesher;
pub mod textured_mesher;
pub use binary_greedy_mesher::BinaryGreedyMesher;
pub use mesher::Mesher;
pub use textured_mesher::TexturedMesher;

View File

@@ -0,0 +1,410 @@
use crate::chunk::chunk::CHUNK_SIZE;
use crate::chunk::{self, Chunk, ChunkMesh};
use crate::editor::voxel_registry::{VoxelModel, VoxelRegistry};
use crate::meshing::Mesher;
use godot::prelude::*;
pub struct TexturedMesher {
face_vertices: [[Vector3; 4]; 6],
face_normals: [Vector3; 6],
}
impl TexturedMesher {
pub fn new() -> Self {
let face_vertices = Self::generate_face_vertices();
let face_normals = [
Vector3::LEFT, // 0
Vector3::RIGHT, // 1
Vector3::DOWN, // 2
Vector3::UP, // 3
Vector3::BACK, // 4
Vector3::FORWARD, // 5
];
Self {
face_vertices,
face_normals,
}
}
fn generate_face_vertices() -> [[Vector3; 4]; 6] {
[
// LEFT (X-)
[
Vector3::new(0.0, 0.0, 1.0),
Vector3::new(0.0, 0.0, 0.0),
Vector3::new(0.0, 1.0, 0.0),
Vector3::new(0.0, 1.0, 1.0),
],
// RIGHT (X+)
[
Vector3::new(1.0, 0.0, 0.0),
Vector3::new(1.0, 0.0, 1.0),
Vector3::new(1.0, 1.0, 1.0),
Vector3::new(1.0, 1.0, 0.0),
],
// DOWN (Y-)
[
Vector3::new(0.0, 0.0, 1.0),
Vector3::new(1.0, 0.0, 1.0),
Vector3::new(1.0, 0.0, 0.0),
Vector3::new(0.0, 0.0, 0.0),
],
// UP (Y+)
[
Vector3::new(0.0, 1.0, 0.0),
Vector3::new(1.0, 1.0, 0.0),
Vector3::new(1.0, 1.0, 1.0),
Vector3::new(0.0, 1.0, 1.0),
],
// BACK (Z+)
[
Vector3::new(1.0, 0.0, 1.0),
Vector3::new(0.0, 0.0, 1.0),
Vector3::new(0.0, 1.0, 1.0),
Vector3::new(1.0, 1.0, 1.0),
],
// FRONT (Z-)
[
Vector3::new(0.0, 0.0, 0.0),
Vector3::new(1.0, 0.0, 0.0),
Vector3::new(1.0, 1.0, 0.0),
Vector3::new(0.0, 1.0, 0.0),
],
]
}
#[inline(always)]
fn is_face_visible(chunk: &Chunk, x: usize, y: usize, z: usize, face: usize) -> bool {
// Convert to i32 for neighbor calculation
let (nx, ny, nz) = match face {
0 => (x as i32 - 1, y as i32, z as i32), // LEFT
1 => (x as i32 + 1, y as i32, z as i32), // RIGHT
2 => (x as i32, y as i32 - 1, z as i32), // DOWN
3 => (x as i32, y as i32 + 1, z as i32), // UP
4 => (x as i32, y as i32, z as i32 + 1), // BACK
5 => (x as i32, y as i32, z as i32 - 1), // FRONT
_ => return false,
};
// Check if neighbor is out of bounds or empty
if nx < 0 || nx >= CHUNK_SIZE || ny < 0 || ny >= CHUNK_SIZE || nz < 0 || nz >= CHUNK_SIZE {
return true;
}
!chunk.get_voxel(
Vector3i::new(nx, ny, nz),
Vector3i {
x: 32,
y: 32,
z: 32,
},
) // Visible if neighbor is air (false)
}
fn add_cube_voxel_to_mesh_fast(
&self,
x: usize,
y: usize,
z: usize,
model: &VoxelModel, // Already borrowed, no binding needed
mesh: &mut ChunkMesh,
chunk: &Chunk,
) {
let pos = Vector3::new(x as f32, y as f32, z as f32);
for face in 0..6 {
if !Self::is_face_visible(chunk, x, y, z, face) {
continue;
}
let base_index = mesh.vertices.len() as i32;
// Add precomputed vertices
for vertex in &self.face_vertices[face] {
mesh.vertices.push(pos + *vertex);
}
// Add normals (all same for this face)
let normal = self.face_normals[face];
for _ in 0..4 {
mesh.normals.push(normal);
}
// Get UVs - precompute this if possible!
let tile_coord = model.get_tile_for_face(face as i32);
let atlas_size = Vector2i::new(16, 16); // Should come from model
let uvs = self.generate_face_uvs_fast(tile_coord, atlas_size);
mesh.uvs.extend_from_slice(&uvs);
// Add indices
mesh.indices.extend_from_slice(&[
base_index,
base_index + 1,
base_index + 2,
base_index + 2,
base_index + 3,
base_index,
]);
}
}
#[inline(always)]
fn generate_face_uvs_fast(&self, tile_coord: Vector2i, atlas_size: Vector2i) -> [Vector2; 4] {
let inv_atlas_x = 1.0 / atlas_size.x as f32;
let inv_atlas_y = 1.0 / atlas_size.y as f32;
let u_min = tile_coord.x as f32 * inv_atlas_x;
let u_max = (tile_coord.x + 1) as f32 * inv_atlas_x;
let v_min = tile_coord.y as f32 * inv_atlas_y;
let v_max = (tile_coord.y + 1) as f32 * inv_atlas_y;
[
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 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 {
fn generate_mesh(&self, chunk: &Chunk, mesh: &mut ChunkMesh) {
panic!("TexturedMesher requires a VoxelRegistry. Use generate_mesh_with_registry instead.");
}
fn generate_mesh_with_registry(
&self,
chunk: &Chunk,
registry: &VoxelRegistry,
mesh: &mut ChunkMesh,
) {
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);
for x in 0..32 {
for y in 0..32 {
for z in 0..32 {
let is_solid = chunk.get_voxel(
Vector3i::new(x, y, z),
Vector3i {
x: 32,
y: 32,
z: 32,
},
);
if !is_solid {
continue;
}
// For now, use model index 1 for all solid voxels
// In the future, you might want to store material types separately
let model = registry.models.at(1);
self.add_cube_voxel_to_mesh_fast(
x as usize,
y as usize,
z as usize,
&model.bind(),
mesh,
chunk,
);
}
}
}
}
fn get_name(&self) -> &str {
"TexturedMesher"
}
}

3
src/rendering/mod.rs Normal file
View File

@@ -0,0 +1,3 @@
pub mod renderer;
pub use renderer::Renderer;

220
src/rendering/renderer.rs Normal file
View File

@@ -0,0 +1,220 @@
use std::{collections::HashMap, time::Instant};
use crate::chunk::{self, Chunk, ChunkMesh};
use godot::{
classes::{
ArrayMesh, Material, MeshInstance3D, ResourceLoader, RenderingServer, StandardMaterial3D,
base_material_3d::{CullMode, Flags, ShadingMode, Transparency},
geometry_instance_3d::ShadowCastingSetting,
mesh::{ArrayType, PrimitiveType},
rendering_server::InstanceType,
},
obj::IndexEnum,
prelude::*,
};
pub struct Renderer {
mesh_instances: HashMap<(i32, i32, i32), Gd<MeshInstance3D>>,
terrain_material: Option<Gd<Material>>,
// Cache for reusable mesh instances
mesh_instance_pool: Vec<Gd<MeshInstance3D>>,
}
impl Renderer {
pub fn new() -> Self {
let terrain_material = Self::load_terrain_material();
Self {
mesh_instances: HashMap::new(),
terrain_material,
mesh_instance_pool: Vec::with_capacity(100),
}
}
#[inline]
fn load_terrain_material() -> Option<Gd<Material>> {
let mut resource_loader = ResourceLoader::singleton();
let path: GString = "res://materials/terrain_material.tres".into();
resource_loader
.load(&path)
.and_then(|resource| resource.try_cast::<Material>().ok())
}
pub fn render_chunk(&mut self, chunk: &Chunk, mesh: &ChunkMesh) -> Gd<MeshInstance3D> {
let chunk_key = chunk.world_position;
let chunk_key_i32 = (chunk_key.0 as i32, chunk_key.1 as i32, chunk_key.2 as i32);
if let Some(old_instance) = self.mesh_instances.remove(&chunk_key_i32) {
old_instance.free();
godot_print!("Renderer: Replaced mesh instance at {:?}", chunk_key);
}
// Skip empty meshes
if mesh.is_empty() {
return MeshInstance3D::new_alloc();
}
// Create Godot arrays for mesh data - use new() instead of new_gd()
let mut array_mesh = ArrayMesh::new_gd();
array_mesh.clear_surfaces();
let mut surface_array: VariantArray = VariantArray::new();
// Convert our mesh data to Godot arrays
surface_array.resize(ArrayType::MAX.to_index(), &Variant::nil()); // Mesh::ARRAY_MAX
// Vertices - push directly without to_variant()
let mut vertices = PackedVector3Array::new();
for vertex in &mesh.vertices {
vertices.push(*vertex);
}
surface_array.set(ArrayType::VERTEX.to_index(), &Variant::from(vertices)); // Remove .to_variant()
// Normals
let mut normals = PackedVector3Array::new();
for normal in &mesh.normals {
normals.push(*normal);
}
surface_array.set(ArrayType::NORMAL.to_index(), &Variant::from(normals)); // Remove .to_variant()
// Colors
let mut colors = PackedColorArray::new();
for color in &mesh.colors {
colors.push(*color);
}
surface_array.set(ArrayType::COLOR.to_index(), &Variant::from(colors)); // Remove .to_variant()
// Indices
let mut indices = PackedInt32Array::new();
for index in &mesh.indices {
indices.push(*index);
}
surface_array.set(ArrayType::INDEX.to_index(), &Variant::from(indices)); // Remove .to_variant()
// Create the mesh - use correct arguments
array_mesh.add_surface_from_arrays(PrimitiveType::TRIANGLES, &surface_array);
// Create mesh instance using new_alloc() for manually managed nodes
let mut mesh_instance = MeshInstance3D::new_alloc();
mesh_instance.set_mesh(&array_mesh);
// Position the mesh instance at the chunk's world position
let (x, y, z) = chunk.world_position;
mesh_instance.set_position(Vector3::new(
(x * 16 as f64) as f32,
(y * 16 as f64) as f32,
(z * 16 as f64) as f32,
));
// Create a proper material that supports shadows
let mut material = StandardMaterial3D::new_gd();
// Enable shadows
material.set_flag(Flags::ALBEDO_FROM_VERTEX_COLOR, true);
material.set_shading_mode(ShadingMode::PER_PIXEL);
material.set_transparency(Transparency::DISABLED);
material.set_cull_mode(CullMode::BACK);
// You can also add some basic properties
material.set_roughness(0.8);
material.set_metallic(0.0);
// Create mesh instance
mesh_instance.set_material_override(&material);
// Enable shadows for the mesh instance
mesh_instance.set_cast_shadows_setting(ShadowCastingSetting::ON);
self.mesh_instances
.insert(chunk_key_i32, mesh_instance.clone());
mesh_instance
}
pub fn render_chunk_with_uvs(&mut self, chunk: &Chunk, mesh: &ChunkMesh) -> Gd<MeshInstance3D> {
let chunk_key = chunk.world_position;
let chunk_key_i32 = (chunk_key.0 as i32, chunk_key.1 as i32, chunk_key.2 as i32);
// Reuse or remove old instance
if let Some(old_instance) = self.mesh_instances.remove(&chunk_key_i32) {
// Return to pool instead of freeing
self.mesh_instance_pool.push(old_instance);
}
if mesh.is_empty() {
return MeshInstance3D::new_alloc();
}
// Create mesh arrays more efficiently
let mut array_mesh = ArrayMesh::new_gd();
let mut surface_array: VariantArray = VariantArray::new();
surface_array.resize(ArrayType::MAX.to_index(), &Variant::nil());
// Use push instead of set for better performance
let mut vertices = PackedVector3Array::new();
for vertex in &mesh.vertices {
vertices.push(*vertex);
}
surface_array.set(ArrayType::VERTEX.to_index(), &Variant::from(vertices));
let mut normals = PackedVector3Array::new();
for normal in &mesh.normals {
normals.push(*normal);
}
surface_array.set(ArrayType::NORMAL.to_index(), &Variant::from(normals));
let mut uvs = PackedVector2Array::new();
for uv in &mesh.uvs {
uvs.push(*uv);
}
surface_array.set(ArrayType::TEX_UV.to_index(), &Variant::from(uvs));
let mut indices = PackedInt32Array::new();
for index in &mesh.indices {
indices.push(*index);
}
surface_array.set(ArrayType::INDEX.to_index(), &Variant::from(indices));
array_mesh.add_surface_from_arrays(PrimitiveType::TRIANGLES, &surface_array);
// Reuse mesh instance from pool or create new
let mut mesh_instance = self.mesh_instance_pool.pop().unwrap_or_else(|| {
let mut instance = MeshInstance3D::new_alloc();
instance.set_cast_shadows_setting(ShadowCastingSetting::ON);
instance
});
mesh_instance.set_mesh(&array_mesh);
// Position the mesh instance (chunk size is now 32)
let (x, y, z) = chunk.world_position;
mesh_instance.set_position(Vector3::new(
(x * 32.0) as f32,
(y * 32.0) as f32,
(z * 32.0) as f32,
));
// Apply material if available (do this without timing)
if let Some(mat) = &self.terrain_material {
mesh_instance.set_material_override(mat);
}
self.mesh_instances
.insert(chunk_key_i32, mesh_instance.clone());
mesh_instance
}
pub fn clear(&mut self) {
for (_, instance) in self.mesh_instances.drain() {
self.mesh_instance_pool.push(instance);
}
self.mesh_instances.clear();
}
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);
}
}
}

7
src/voxel/mod.rs Normal file
View File

@@ -0,0 +1,7 @@
pub mod model;
pub mod registry;
pub mod voxel;
pub use model::*;
pub use registry::VoxelRegistry;
pub use voxel::Voxel;

24
src/voxel/model.rs Normal file
View File

@@ -0,0 +1,24 @@
use godot::classes::class_macros::private::virtuals::Os::Vector2i;
#[derive(Debug, PartialEq, Clone, Copy)]
#[repr(u8)]
pub enum VoxelModelType {
Empty = 0_u8,
Cube,
Mesh,
Fluid,
}
#[derive(Debug, Clone, Copy)]
pub struct VoxelModel {
voxel_type: VoxelModelType,
atlas_size_in_tiles: Vector2i,
tile_left: Vector2i,
tile_right: Vector2i,
tile_bottom: Vector2i,
tile_top: Vector2i,
tile_back: Vector2i,
tile_front: Vector2i,
}

13
src/voxel/registry.rs Normal file
View File

@@ -0,0 +1,13 @@
use crate::voxel::VoxelModel;
pub struct VoxelRegistry {
pub models: Vec<VoxelModel>,
}
impl VoxelRegistry {
pub fn new(size: usize) -> Self {
VoxelRegistry {
models: Vec::with_capacity(size),
}
}
}

33
src/voxel/voxel.rs Normal file
View File

@@ -0,0 +1,33 @@
use godot::classes::class_macros::private::virtuals::Os::Color;
#[derive(Copy, Clone, Debug, PartialEq, Default)]
pub enum Voxel {
#[default]
Air = 0,
Grass,
Dirt,
Stone,
Sand,
Water,
}
impl Voxel {
pub fn get_color(&self) -> Color {
match self {
Voxel::Air => Color::from_rgba(0.0, 0.0, 0.0, 0.0), // Transparent
Voxel::Grass => Color::from_rgb(0.2, 0.8, 0.2),
Voxel::Dirt => Color::from_rgb(0.5, 0.3, 0.1),
Voxel::Stone => Color::from_rgb(0.4, 0.4, 0.4),
Voxel::Sand => Color::from_rgb(0.9, 0.8, 0.5),
Voxel::Water => Color::from_rgba(0.2, 0.4, 0.8, 0.7),
}
}
pub fn is_solid(&self) -> bool {
match self {
Voxel::Air => false,
Voxel::Water => false, // Or true if you want solid water
_ => true,
}
}
}

176
src/world.rs Normal file
View File

@@ -0,0 +1,176 @@
use godot::classes::{INode3D, VoxelGi};
use godot::prelude::*;
use crate::chunk::ChunkManager;
use crate::chunk::chunk::CHUNK_SIZE;
use crate::editor::VoxelRegistry;
use crate::generation::{HeightmapGenerator, WorldGenerator};
use crate::meshing::mesher::CullingMesher;
use crate::meshing::{BinaryGreedyMesher, Mesher};
#[derive(GodotClass)]
#[class(base=Node3D,tool)]
struct World {
base: Base<Node3D>,
// ===== RENDERING GROUP =====
#[export_group(name = "Rendering")]
#[export]
chunk_size: Vector3i,
#[export(range = (2.0,64.0))]
render_distance: u8,
// ===== GENERATION GROUP =====
#[export_group(name = "Generation")]
#[export]
world_seed: i32,
#[export(range = (0.01, 4.0, 0.01))]
noise_frequency: f32,
#[export(range = (1.0, 10.0))]
terrain_height: u32,
#[export(range = (1.0, 64.0, 1.0))]
workder_threads: u8,
#[export_group(name = "Voxel Registry")]
#[export]
registry: Option<Gd<VoxelRegistry>>,
#[export_group(name = "GI")]
#[export]
voxelgi_node: Option<Gd<VoxelGi>>,
// World data (temporary)
chunk_manager: ChunkManager,
generator: Box<dyn WorldGenerator>,
mesher: Box<dyn Mesher>,
}
#[godot_api]
impl INode3D for World {
fn init(base: Base<Node3D>) -> Self {
godot_print!("🧊 Hello from FastVoxel");
let generator = Box::new(HeightmapGenerator::new(1234, 0.03, 5));
let mesher = Box::new(CullingMesher::new());
let chunk_manager = ChunkManager::new();
Self {
render_distance: 8,
chunk_size: Vector3i::new(16, 16, 16),
world_seed: 1234,
noise_frequency: 0.03,
terrain_height: 5,
base,
generator,
mesher,
chunk_manager,
workder_threads: 4,
registry: None,
voxelgi_node: None,
}
}
fn process(&mut self, _delta: f64) {
self.add_pending_mesh_instances_to_scene();
}
fn ready(&mut self) {
if self.registry.is_none() {
godot_error!(
"VoxelRegistry not assigned! Please assign a VoxelRegistry resource in the editor."
);
return;
}
if self.voxelgi_node.is_none() {
godot_error!("VoxelGI Node is not selected.");
return;
}
godot_print!("ChunkSize is {0}", self.chunk_size);
self.generator = Box::new(HeightmapGenerator::new(
self.get_world_seed() as i64,
self.noise_frequency,
self.terrain_height as i32,
));
self.chunk_manager.chunk_size = self.chunk_size;
self.chunk_manager.terrain_height = self.terrain_height;
self.regenerate_terrain();
godot_print!("World ready! building GI...");
}
}
#[godot_api]
impl World {
#[func]
fn generate_terrain(&mut self) {
match &self.registry {
None => {
godot_print!("No Voxel registry is provided to the world.")
}
Some(registry) => {
let distance = self.render_distance as i32;
let library_ref = registry.bind();
for x in -distance..=distance {
for z in -distance..=distance {
self.chunk_manager.generate_chunk_column(
x,
z,
&library_ref,
self.generator.as_ref(),
self.mesher.as_ref(),
);
}
}
godot_print!("Textured terrain regenerated!");
}
}
}
fn regenerate_terrain(&mut self) {
godot_print!("Regenerating terrain...");
self.chunk_manager.clear();
// Create textured mesher
self.mesher = Box::new(CullingMesher::new());
self.generate_terrain();
}
#[func]
fn update_from_gdscript(&mut self, player_world_pos: Vector3) {
match &self.registry {
None => return,
Some(registry) => {
let registry_ref = registry.bind();
self.chunk_manager.update_around_player(
player_world_pos,
self.render_distance as i32,
&registry_ref,
self.generator.as_ref(),
self.mesher.as_ref(),
);
}
}
}
fn add_pending_mesh_instances_to_scene(&mut self) {
let mesh_instances = self.chunk_manager.take_pending_mesh_instances();
if mesh_instances.is_empty() {
return;
}
let mut base = self.base_mut();
for mesh_instance in mesh_instances {
base.add_child(&mesh_instance.upcast::<Node3D>());
}
}
}