re-working the editor nodes

de-coupling godot resources from internal runtime logic
This commit is contained in:
2026-03-28 03:12:31 +03:30
parent 166f9e4aa7
commit d3c1b7bb5b
21 changed files with 393 additions and 235 deletions

View File

@@ -4,22 +4,26 @@
<img src="./logo-cropped.svg" alt="FastVoxel logo" width="180" /> <img src="./logo-cropped.svg" alt="FastVoxel logo" width="180" />
</p> </p>
FastVoxel is a voxel terrain engine for Godot written as a Rust GDExtension. The goal is basically: generate chunks quickly, keep voxel storage lightweight, and build meshes at runtime for blocky worlds with textured materials. **FastVoxel** is a **Voxel Engine** for Godot written as a Rust GDExtension. The goal is basically: generate chunks quickly, keep voxel storage lightweight, and build meshes at runtime for blocky worlds with textured materials.
This repo contains the engine/plugin side of the project. This repo contains the engine/GDExtension side of the project.
It's currently being refactored a bit, so some internal structure may change, but the main ideas and APIs are stable enough to explain here. It's currently being refactored constantly, so some internal structure may change, but the main ideas and APIs are stable enough to explain here. (or Are They?)
## Highlights ## Highlights
- Rust-based Godot 4 GDExtension - Rust-based Godot 4 GDExtension
- chunked voxel terrain pipeline - chunked voxel mesh pipeline
- bit-packed voxel storage (solid / air) - bit-packed voxel storage (solid / air)
- procedural terrain using `fastnoise-lite` - procedural terrain using `fastnoise-lite`
- chunk streaming around the player - chunk streaming around the player
- runtime cube meshing with texture atlas support - runtime cube meshing with texture atlas support
- Godot editor resources for config + voxel registry - Godot editor resources for config + voxel registry
## Soon:
- Multi-Threaded chunk meshing and world generation using a `Work Stealing` Thread pool with divideandconquer parallelism.
## Screenshots ## Screenshots
<p align="center"> <p align="center">
@@ -63,6 +67,12 @@ Rough pipeline looks like this:
The idea is to keep generation, storage, and meshing fairly modular so different strategies can be swapped in later. The idea is to keep generation, storage, and meshing fairly modular so different strategies can be swapped in later.
## What FastVoxel "Doesn't" do
1. The engine doesn't use compute shaders or any kind of GPU accelerated meshing algorithem.
2. The engine doesn't bake per-vertex AO on greedy mesher (because it complicates things and I'm running on two brain cells at the moment)
3. The engine doesn't cull the neighboring faces because it has no access to the data of adjacent chunks. (I'm working on it).
## Core Concepts ## Core Concepts
### Chunked world layout ### Chunked world layout
@@ -73,7 +83,7 @@ Current defaults:
- `CHUNK_SIZE = 32` - `CHUNK_SIZE = 32`
- each chunk = `32 × 32 × 32` voxels - each chunk = `32 × 32 × 32` voxels
- columns stack multiple chunks vertically - columns stack multiple chunks vertically like a hamburger
- chunk loading/unloading happens around the player based on render distance - chunk loading/unloading happens around the player based on render distance
Relevant files: Relevant files:
@@ -84,14 +94,18 @@ Relevant files:
### Bit-packed voxel storage ### Bit-packed voxel storage
Instead of storing a struct per voxel, chunks store occupancy using packed `u32` blocks. Instead of storing a struct per voxel, chunks store occupancy using packed `u32` blocks. since a voxel is represented by a single **bit**, a single `u32` can store the state of `32 voxels`.
So right now a voxel is basically: right now a voxel is basically:
- `0` -> air - `0` -> air
- `1` -> solid - `1` -> solid
This keeps memory usage low and makes lookups very cheap. It works well for early terrain prototypes and simple block worlds. This keeps memory usage low and makes lookups very cheap.
For a 32 × 32 × 32 chunk, each horizontal row of 32 voxels fits into a single u32. Which means a layer of the chunk requires 32 u32s, and the entire chunk can be stored as a 32 × 32 array of u32s. Each u32 corresponds to one row of voxels along the X axis.
This compact memory layout works well for terrain meshing stage, where the main concern is to quickly check whether voxels are empty or solid. because we use a single bit to represent a voxel instead of a full struct or enum or whatever, the implementation is 32× more memory efficient (no sh- sherlock).
Material differences are currently handled at the meshing/registry layer instead of inside the voxel storage itself. Material differences are currently handled at the meshing/registry layer instead of inside the voxel storage itself.

View File

@@ -2,11 +2,12 @@ use godot::prelude::*;
pub const CHUNK_SIZE: i32 = 32; pub const CHUNK_SIZE: i32 = 32;
pub const ARR_SIZE: usize = CHUNK_SIZE.pow(3) as usize; pub const ARR_SIZE: usize = CHUNK_SIZE.pow(3) as usize;
// 32x32x32 = 32768 bits, stored in u32s (32 bits each) = 1024 u32s // 32x32x32 = 32768 bits, stored in u32s (32 bits each) = 1024 u32s
pub const BITPACKED_SIZE: usize = ARR_SIZE / 32; pub const BITPACKED_SIZE: usize = ARR_SIZE / 32;
#[derive(Debug)] #[derive(Debug)]
pub struct Chunk { pub struct SubChunk {
// Bitpacked voxel data: 0 = air, 1 = solid // Bitpacked voxel data: 0 = air, 1 = solid
pub voxels: Vec<u32>, pub voxels: Vec<u32>,
// WORLD COORDINATES // WORLD COORDINATES
@@ -14,10 +15,10 @@ pub struct Chunk {
modified: bool, modified: bool,
} }
impl Chunk { impl SubChunk {
/// 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) -> Self { pub fn new(world_pos_x: f64, world_pos_y: f64, world_pos_z: f64) -> Self {
Chunk { SubChunk {
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),
modified: false, modified: false,
@@ -30,7 +31,7 @@ impl Chunk {
// as usize // as usize
// } // }
#[inline(always)] #[inline(always)]
fn get_voxel_index(local: Vector3i, chunk_size: Vector3i) -> usize { fn get_voxel_index(local: Vector3i) -> usize {
((local.x << 10) | (local.y << 5) | local.z) as usize ((local.x << 10) | (local.y << 5) | local.z) as usize
} }
@@ -52,7 +53,7 @@ impl Chunk {
local_pos local_pos
); );
let index = Self::get_voxel_index(local_pos, chunk_size); let index = Self::get_voxel_index(local_pos);
debug_assert!( debug_assert!(
index < BITPACKED_SIZE, index < BITPACKED_SIZE,
"Voxel index {:?} out of chunk bounds", "Voxel index {:?} out of chunk bounds",
@@ -71,7 +72,7 @@ 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);
let word_index = index >> 5; let word_index = index >> 5;
let bit_index = index & 31; let bit_index = index & 31;

View File

@@ -3,17 +3,16 @@ use godot::classes::class_macros::private::virtuals::Os::{Vector3, Vector3i};
use godot::global::godot_print; use godot::global::godot_print;
use godot::obj::Gd; use godot::obj::Gd;
use crate::chunk::{Chunk, ChunkColumn, ChunkMesh}; use crate::chunk::{Chunk, ChunkMesh, SubChunk};
use crate::editor::voxel_registry::VoxelRegistry; use crate::editor::voxel_registry::VoxelRegistry;
use crate::generation::SimpleSurfaceGenerator; use crate::generation::generator::TerrainGenerator;
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;
use std::time::Instant; use std::time::Instant;
pub struct ChunkManager { pub struct ChunkManager {
pub chunk_columns: HashMap<(i32, i32), ChunkColumn>, pub chunk_columns: HashMap<(i32, i32), Chunk>,
pub renderer: Renderer, pub renderer: Renderer,
pub last_player_chunk: (i32, i32), pub last_player_chunk: (i32, i32),
@@ -43,7 +42,7 @@ impl ChunkManager {
player_world_pos: Vector3, player_world_pos: Vector3,
render_distance: i32, render_distance: i32,
registry: &VoxelRegistry, registry: &VoxelRegistry,
surface_generator: &dyn SurfaceGenerator, surface_generator: &dyn TerrainGenerator,
mesher: &dyn Mesher, mesher: &dyn Mesher,
) -> bool { ) -> bool {
let player_chunk_index_x = (player_world_pos.x as i32).div_euclid(self.chunk_size.x); let player_chunk_index_x = (player_world_pos.x as i32).div_euclid(self.chunk_size.x);
@@ -91,7 +90,7 @@ impl ChunkManager {
center_z: i32, center_z: i32,
distance: i32, distance: i32,
registry: &VoxelRegistry, registry: &VoxelRegistry,
generator: &dyn SurfaceGenerator, generator: &dyn TerrainGenerator,
mesher: &dyn Mesher, mesher: &dyn Mesher,
) { ) {
use std::collections::HashSet; use std::collections::HashSet;
@@ -129,21 +128,21 @@ impl ChunkManager {
index_x: i32, index_x: i32,
index_z: i32, index_z: i32,
registry: &VoxelRegistry, registry: &VoxelRegistry,
generator: &dyn SurfaceGenerator, generator: &dyn TerrainGenerator,
mesher: &dyn Mesher, mesher: &dyn Mesher,
) { ) {
let mut column = ChunkColumn::new(index_x, index_z, self.chunk_size); let mut column = Chunk::new(index_x, index_z, self.chunk_size);
let start = Instant::now(); let start = Instant::now();
generator.generate(&mut column); generator.sample_chunk(&mut column);
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);
// Mesh and render each chunk // Mesh and render each chunk
for i in 0..self.terrain_height { for i in 0..self.terrain_height {
if let Some(chunk) = column.get_chunk(i as usize) { if let Some(chunk) = column.get_sub_chunk(i as usize) {
self.mesh_and_render_chunk(chunk, mesher, registry); self.mesh_and_render_chunk(chunk, mesher, registry);
} }
} }
@@ -154,7 +153,7 @@ impl ChunkManager {
fn mesh_and_render_chunk( fn mesh_and_render_chunk(
&mut self, &mut self,
chunk: &Chunk, chunk: &SubChunk,
mesher: &dyn Mesher, mesher: &dyn Mesher,
registry: &VoxelRegistry, registry: &VoxelRegistry,
) { ) {

View File

@@ -1,26 +1,31 @@
use godot::classes::class_macros::private::virtuals::Os::Vector3i; use godot::classes::class_macros::private::virtuals::Os::Vector3i;
use super::chunk::{CHUNK_SIZE, Chunk}; use super::chunk::{CHUNK_SIZE, SubChunk};
pub const CHUNKS_PER_COLUMN: usize = 8; pub const CHUNKS_PER_COLUMN: usize = 8;
pub struct ChunkColumn { pub struct ChunkPos {
pub chunks: [Option<Chunk>; CHUNKS_PER_COLUMN], pub x: i64,
pub y: i64,
}
pub struct Chunk {
pub sub_chunks: [Option<SubChunk>; CHUNKS_PER_COLUMN],
pub world_position: (i32, i32), // XZ pub world_position: (i32, i32), // XZ
pub chunk_size: Vector3i, pub chunk_size: Vector3i,
} }
impl ChunkColumn { impl Chunk {
pub fn new(x: i32, z: i32, chunk_size: Vector3i) -> Self { pub fn new(x: i32, z: i32, chunk_size: Vector3i) -> Self {
ChunkColumn { Chunk {
chunks: [None, None, None, None, None, None, None, None], sub_chunks: [None, None, None, None, None, None, None, None],
world_position: (x, z), world_position: (x, z),
chunk_size, chunk_size,
} }
} }
#[inline] #[inline]
pub fn get_chunk_index(chunk_size_y: i32, world_y: i32) -> usize { pub fn get_sub_chunk_index(chunk_size_y: i32, world_y: i32) -> usize {
(world_y / chunk_size_y) as usize (world_y / chunk_size_y) as usize
} }
@@ -34,31 +39,31 @@ impl ChunkColumn {
(chunk_index * chunk_size_y) + local_y (chunk_index * chunk_size_y) + local_y
} }
pub fn get_or_create_chunk(&mut self, chunk_y_index: i32) -> &mut Chunk { pub fn get_or_create_sub_chunk(&mut self, chunk_y_index: i32) -> &mut SubChunk {
if self.chunks[chunk_y_index as usize].is_none() { if self.sub_chunks[chunk_y_index as usize].is_none() {
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.sub_chunks[chunk_y_index as usize] = Some(SubChunk::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.chunks[chunk_y_index as usize].as_mut().unwrap() self.sub_chunks[chunk_y_index as usize].as_mut().unwrap()
} }
pub fn get_chunk(&self, chunk_y_index: usize) -> Option<&Chunk> { pub fn get_sub_chunk(&self, chunk_y_index: usize) -> Option<&SubChunk> {
self.chunks[chunk_y_index].as_ref() self.sub_chunks[chunk_y_index].as_ref()
} }
pub fn get_chunk_mut(&mut self, chunk_y_index: usize) -> Option<&mut Chunk> { pub fn get_sub_chunk_mut(&mut self, chunk_y_index: usize) -> Option<&mut SubChunk> {
self.chunks[chunk_y_index].as_mut() self.sub_chunks[chunk_y_index].as_mut()
} }
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_sub_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; let size = self.chunk_size;
let chunk = self.get_or_create_chunk(chunk_index as i32); let chunk = self.get_or_create_sub_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);
let local_y = Self::get_local_y(size.y, world_pos.y); let local_y = Self::get_local_y(size.y, world_pos.y);
@@ -72,8 +77,8 @@ impl ChunkColumn {
} }
pub fn get_voxel(&self, world_pos: Vector3i) -> bool { pub fn get_voxel(&self, world_pos: Vector3i) -> bool {
let chunk_index = Self::get_chunk_index(self.chunk_size.y, world_pos.y); let chunk_index = Self::get_sub_chunk_index(self.chunk_size.y, world_pos.y);
if let Some(chunk) = self.get_chunk(chunk_index) { if let Some(chunk) = self.get_sub_chunk(chunk_index) {
let local_x = world_pos.x.rem_euclid(CHUNK_SIZE); 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_y = Self::get_local_y(self.chunk_size.y, world_pos.y);
let local_z = world_pos.z.rem_euclid(CHUNK_SIZE); let local_z = world_pos.z.rem_euclid(CHUNK_SIZE);

View File

@@ -3,7 +3,7 @@ pub mod chunk_manager;
pub mod column; pub mod column;
pub mod mesh; pub mod mesh;
pub use chunk::Chunk; pub use chunk::SubChunk;
pub use chunk_manager::ChunkManager; pub use chunk_manager::ChunkManager;
pub use column::ChunkColumn; pub use column::Chunk;
pub use mesh::ChunkMesh; pub use mesh::ChunkMesh;

View File

@@ -1,5 +1,6 @@
pub mod voxel_registry; pub mod voxel_registry;
pub mod world_config; pub mod world_config;
pub mod world_generator;
pub mod world_node; pub mod world_node;
pub mod world_plugin; pub mod world_plugin;

View File

@@ -1,9 +1,11 @@
use godot::{ use godot::{
classes::{IResource, Resource, class_macros::private::virtuals::Os::Vector3i}, classes::{IResource, Resource},
obj::Base, obj::{Base, Gd},
prelude::{GodotClass, godot_api}, prelude::{GodotClass, godot_api},
}; };
use crate::editor::world_generator::WorldGeneratorResource;
#[derive(GodotClass)] #[derive(GodotClass)]
#[class(base=Resource)] #[class(base=Resource)]
pub struct WorldConfig { pub struct WorldConfig {
@@ -11,9 +13,6 @@ pub struct WorldConfig {
// ===== WORLD GROUP ===== // ===== WORLD GROUP =====
#[export_group(name = "World")] #[export_group(name = "World")]
#[export]
chunk_size: Vector3i,
#[export(range = (2.0,64.0))] #[export(range = (2.0,64.0))]
render_distance: u8, render_distance: u8,
@@ -23,13 +22,10 @@ pub struct WorldConfig {
// ===== GENERATION GROUP ===== // ===== GENERATION GROUP =====
#[export_group(name = "Generation")] #[export_group(name = "Generation")]
#[export] #[export]
seed: i32, generator: Option<Gd<WorldGeneratorResource>>,
#[export(range = (0.01, 4.0, 0.01))]
frequency: f32,
#[export(range = (1.0, 10.0))] #[export(range = (1.0, 10.0))]
terrain_height: u32, surface_height: u8,
} }
#[godot_api] #[godot_api]
@@ -38,12 +34,11 @@ impl IResource for WorldConfig {
// DEFAULT Values // DEFAULT Values
Self { Self {
base, base,
chunk_size: Vector3i::new(32, 32, 32),
render_distance: 8, render_distance: 8,
worker_threads: 4, worker_threads: 8,
seed: 1234, generator: None,
frequency: 0.03, // noise: None,
terrain_height: 6, surface_height: 6,
} }
} }
} }

View File

@@ -0,0 +1,154 @@
use godot::{
classes::{FastNoiseLite, Resource},
obj::{Base, Gd},
prelude::{Export, GodotClass, GodotConvert, Var},
};
use crate::runtime::types::IntoRuntime;
use fastnoise_lite::FastNoiseLite as RustNoise;
#[derive(GodotConvert, Var, Export, Debug, Clone, Copy, PartialEq)]
#[godot(via = i64)]
pub enum WorldGeneratorType {
Noise2D,
Noise3D,
Graph,
Flat,
}
impl Default for WorldGeneratorType {
fn default() -> Self {
WorldGeneratorType::Noise2D
}
}
#[derive(GodotClass, Debug)]
#[class(init, base=Resource)]
pub struct WorldGeneratorResource {
#[base]
base: Base<Resource>,
#[export]
pub noise2d: Option<Gd<FastNoiseLite>>,
}
impl WorldGeneratorResource {
fn convert_noise_type(
ty: godot::classes::fast_noise_lite::NoiseType,
) -> fastnoise_lite::NoiseType {
use fastnoise_lite::NoiseType as R;
use godot::classes::fast_noise_lite::NoiseType as G;
match ty {
G::SIMPLEX => R::OpenSimplex2,
G::SIMPLEX_SMOOTH => R::OpenSimplex2S,
G::PERLIN => R::Perlin,
G::VALUE => R::Value,
G::VALUE_CUBIC => R::ValueCubic,
G::CELLULAR => R::Cellular,
_ => R::OpenSimplex2S, // reasonable default
}
}
fn convert_fractal_type(
ft: godot::classes::fast_noise_lite::FractalType,
dft: godot::classes::fast_noise_lite::DomainWarpFractalType,
) -> fastnoise_lite::FractalType {
use fastnoise_lite::FractalType as R;
use godot::classes::fast_noise_lite::DomainWarpFractalType as D;
use godot::classes::fast_noise_lite::FractalType as G;
// Priority: domain-warp fractals first
let domain_fractal = match dft {
D::PROGRESSIVE => Some(R::DomainWarpProgressive),
D::INDEPENDENT => Some(R::DomainWarpIndependent),
D::NONE => None,
_ => None,
};
if let Some(df) = domain_fractal {
return df;
}
// Then normal fractals
match ft {
G::NONE => R::None,
G::FBM => R::FBm,
G::RIDGED => R::Ridged,
G::PING_PONG => R::PingPong,
_ => R::None,
}
}
fn convert_cellular_distance_function(
fnc: godot::classes::fast_noise_lite::CellularDistanceFunction,
) -> fastnoise_lite::CellularDistanceFunction {
use fastnoise_lite::CellularDistanceFunction as R;
use godot::classes::fast_noise_lite::CellularDistanceFunction as G;
match fnc {
G::EUCLIDEAN => R::Euclidean,
G::EUCLIDEAN_SQUARED => R::EuclideanSq,
G::MANHATTAN => R::Manhattan,
G::HYBRID => R::Hybrid,
_ => R::Euclidean,
}
}
fn convert_cellular_return_type(
ret: godot::classes::fast_noise_lite::CellularReturnType,
) -> fastnoise_lite::CellularReturnType {
use fastnoise_lite::CellularReturnType as R;
use godot::classes::fast_noise_lite::CellularReturnType as G;
match ret {
G::CELL_VALUE => R::CellValue,
G::DISTANCE => R::Distance,
G::DISTANCE2 => R::Distance2,
G::DISTANCE2_ADD => R::Distance2Add,
G::DISTANCE2_SUB => R::Distance2Sub,
G::DISTANCE2_MUL => R::Distance2Mul,
G::DISTANCE2_DIV => R::Distance2Div,
_ => R::CellValue,
}
}
fn convert_domain_warp_type(
warp: godot::classes::fast_noise_lite::DomainWarpType,
) -> fastnoise_lite::DomainWarpType {
use fastnoise_lite::DomainWarpType as R;
use godot::classes::fast_noise_lite::DomainWarpType as G;
match warp {
G::SIMPLEX => R::OpenSimplex2,
G::SIMPLEX_REDUCED => R::OpenSimplex2Reduced,
G::BASIC_GRID => R::BasicGrid,
_ => R::OpenSimplex2,
}
}
}
impl IntoRuntime<RustNoise> for WorldGeneratorResource {
fn into_runtime(&self) -> RustNoise {
match &self.noise2d {
Some(noise) => {
let mut rn = RustNoise::new();
rn.set_seed(Some(noise.get_seed()));
rn.set_frequency(Some(noise.get_frequency()));
rn.set_noise_type(Some(Self::convert_noise_type(noise.get_noise_type())));
rn.set_fractal_type(Some(Self::convert_fractal_type(
noise.get_fractal_type(),
noise.get_domain_warp_fractal_type(),
)));
rn.set_cellular_distance_function(Some(Self::convert_cellular_distance_function(
noise.get_cellular_distance_function(),
)));
rn.set_cellular_return_type(Some(Self::convert_cellular_return_type(
noise.get_cellular_return_type(),
)));
rn.set_domain_warp_type(Some(Self::convert_domain_warp_type(
noise.get_domain_warp_type(),
)));
return rn;
}
None => return RustNoise::new(),
}
}
}

View File

@@ -1,12 +1,9 @@
use godot::classes::{INode3D, VoxelGi}; use godot::classes::INode3D;
use godot::prelude::*; use godot::prelude::*;
use crate::editor::VoxelRegistry; use crate::editor::VoxelRegistry;
use crate::editor::world_config::WorldConfig; use crate::editor::world_config::WorldConfig;
use crate::generation::SimpleSurfaceGenerator; use crate::runtime::Runtime;
use crate::generation::generator::SurfaceGenerator;
use crate::meshing::{Mesher, TexturedMesher};
use crate::terrain::TerrainManager;
#[derive(GodotClass)] #[derive(GodotClass)]
#[class(base=Node3D,tool)] #[class(base=Node3D,tool)]
@@ -23,10 +20,12 @@ pub struct World {
#[export] #[export]
registry: Option<Gd<VoxelRegistry>>, registry: Option<Gd<VoxelRegistry>>,
// runtime systems state: WorldState,
terrain_manager: TerrainManager, }
runtime_generator: Box<dyn SurfaceGenerator>,
runtime_mesher: Box<dyn Mesher>, pub enum WorldState {
Uninitialized,
Ready { runtime: Runtime },
} }
#[godot_api] #[godot_api]
@@ -37,36 +36,33 @@ impl INode3D for World {
base, base,
config: None, config: None,
registry: None, registry: None,
terrain_manager: TerrainManager::new(), state: WorldState::Uninitialized,
runtime_generator: Box::new(SimpleSurfaceGenerator::defaults()),
runtime_mesher: Box::new(TexturedMesher::new()),
} }
} }
fn process(&mut self, _delta: f64) { fn process(&mut self, _delta: f64) {
self.add_pending_mesh_instances_to_scene(); // self.add_pending_mesh_instances_to_scene();
} }
fn ready(&mut self) { fn ready(&mut self) {
if self.registry.is_none() { self.initialize_runtime();
godot_error!(
"VoxelRegistry not assigned! Please assign a VoxelRegistry resource in the editor."
);
return;
} }
}
let surface_generator = Box::new(SimpleSurfaceGenerator::new( impl World {
self.world_seed, fn initialize_runtime(&mut self) {
self.noise_frequency, match (&self.registry, &self.config) {
self.terrain_height, (Some(reg), Some(cfg)) => {
self.chunk_size, let cfg_ref = cfg.bind();
)); let reg_ref = reg.bind();
self.surface_generator = surface_generator; let runtime = Runtime::new(cfg_ref, reg_ref);
self.chunk_manager.chunk_size = self.chunk_size; self.state = WorldState::Ready { runtime };
self.chunk_manager.terrain_height = self.terrain_height; }
_ => {
self.regenerate_terrain(); godot_error!("World: missing resources (config or registry).");
godot_print!("World ready! building GI..."); self.state = WorldState::Uninitialized;
}
}
} }
} }
@@ -76,100 +72,82 @@ impl INode3D for World {
***/ ***/
#[godot_api] #[godot_api]
impl World { impl World {
fn apply_config(&mut self) {
let Some(config) = &self.config else {
godot_error!("VoxelWorld: Missing WorldConfig resource");
return;
};
let cfg = config.bind();
self.chunk_manager.chunk_size = cfg.chunk_size;
self.chunk_manager.render_distance = cfg.render_distance;
self.chunk_manager.terrain_height = cfg.height;
self.generator = Box::new(SimpleSurfaceGenerator::new(
cfg.seed,
cfg.frequency,
cfg.height,
cfg.chunk_size,
));
}
#[func] #[func]
fn generate_terrain(&mut self) { fn generate_world(&mut self, center_pos: Vector3i) {}
match &self.registry { // #[func]
None => { // fn generate_terrain(&mut self) {
godot_print!("No Voxel registry is provided to the world.") // match &self.registry {
} // None => {
Some(registry) => { // godot_print!("No Voxel registry is provided to the world.")
let distance = self.render_distance as i32; // }
let library_ref = registry.bind(); // Some(registry) => {
// let distance = self.render_distance as i32;
// let library_ref = registry.bind();
for index_x in -distance..=distance { // for index_x in -distance..=distance {
for index_z in -distance..=distance { // for index_z in -distance..=distance {
self.chunk_manager.generate_chunk_column( // self.chunk_manager.generate_chunk_column(
index_x, // index_x,
index_z, // index_z,
&library_ref, // &library_ref,
self.surface_generator.as_ref(), // self.surface_generator.as_ref(),
self.mesher.as_ref(), // self.mesher.as_ref(),
); // );
} // }
} // }
godot_print!("Textured terrain regenerated!"); // godot_print!("Textured terrain regenerated!");
} // }
} // }
} // }
#[func] // #[func]
fn regenerate_terrain(&mut self) { // fn regenerate_terrain(&mut self) {
godot_print!("Regenerating terrain..."); // godot_print!("Regenerating terrain...");
self.chunk_manager.clear(); // self.chunk_manager.clear();
// Create textured mesher // // Create textured mesher
self.mesher = Box::new(TexturedMesher::new()); // self.mesher = Box::new(TexturedMesher::new());
self.generate_terrain(); // self.generate_terrain();
} // }
#[func] // #[func]
fn clear_terrain(&mut self) { // fn clear_terrain(&mut self) {
godot_print!("Clearing Terrain..."); // godot_print!("Clearing Terrain...");
self.chunk_manager.clear(); // self.chunk_manager.clear();
self.chunk_manager.pending_mesh_instances.clear(); // self.chunk_manager.pending_mesh_instances.clear();
self.chunk_manager.renderer.clear_and_destroy(); // self.chunk_manager.renderer.clear_and_destroy();
} // }
#[func] // #[func]
fn update_from_gdscript(&mut self, player_world_pos: Vector3) { // fn update_from_gdscript(&mut self, player_world_pos: Vector3) {
match &self.registry { // match &self.registry {
None => return, // None => return,
Some(registry) => { // Some(registry) => {
let registry_ref = registry.bind(); // let registry_ref = registry.bind();
self.chunk_manager.update_around_player( // self.chunk_manager.update_around_player(
player_world_pos, // player_world_pos,
self.render_distance as i32, // self.render_distance as i32,
&registry_ref, // &registry_ref,
self.surface_generator.as_ref(), // self.surface_generator.as_ref(),
self.mesher.as_ref(), // self.mesher.as_ref(),
); // );
} // }
} // }
} // }
fn add_pending_mesh_instances_to_scene(&mut self) { // fn add_pending_mesh_instances_to_scene(&mut self) {
let mesh_instances = self.chunk_manager.take_pending_mesh_instances(); // let mesh_instances = self.chunk_manager.take_pending_mesh_instances();
if mesh_instances.is_empty() { // if mesh_instances.is_empty() {
return; // return;
} // }
let mut base = self.base_mut(); // let mut base = self.base_mut();
for mesh_instance in mesh_instances { // for mesh_instance in mesh_instances {
base.add_child(&mesh_instance.upcast::<Node3D>()); // base.add_child(&mesh_instance.upcast::<Node3D>());
} // }
} // }
} }

View File

@@ -1,15 +1,6 @@
use godot::classes::class_macros::private::virtuals::Os::Vector3i; use crate::chunk::Chunk;
use crate::chunk::{Chunk, ChunkColumn, column}; pub trait TerrainGenerator: Send + Sync {
fn sample_chunk(&self, column: &mut Chunk);
pub trait WorldGenerator: Send + Sync { fn sample_height(&self, x: f32, z: f32) -> f32;
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;
}
pub trait SurfaceGenerator: Send + Sync {
fn generate(&self, column: &mut ChunkColumn);
fn sample_voxel(&self, pos: Vector3i) -> bool;
} }

View File

@@ -1,5 +1,4 @@
pub mod generator; pub mod generator;
pub mod simple_heightmap; pub mod simple_heightmap;
pub use generator::WorldGenerator;
pub use simple_heightmap::SimpleSurfaceGenerator; pub use simple_heightmap::SimpleSurfaceGenerator;

View File

@@ -1,16 +1,15 @@
use fastnoise_lite::{FastNoiseLite, FractalType}; use fastnoise_lite::{FastNoiseLite, FractalType};
use godot::{classes::class_macros::private::virtuals::Os::Vector3i, global::pow}; use godot::classes::class_macros::private::virtuals::Os::Vector3i;
use crate::{chunk::ChunkColumn, generation::generator::SurfaceGenerator}; use crate::chunk::{Chunk, chunk::CHUNK_SIZE};
pub struct SimpleSurfaceGenerator { pub struct SimpleSurfaceGenerator {
noise: FastNoiseLite, noise: FastNoiseLite,
surface_height: u32, surface_height: u32,
chunk_size: Vector3i,
} }
impl SimpleSurfaceGenerator { impl SimpleSurfaceGenerator {
pub fn new(seed: i32, frequency: f32, surface_height: u32, chunk_size: Vector3i) -> Self { pub fn new(seed: i32, frequency: f32, surface_height: u32) -> Self {
let mut noise = FastNoiseLite::new(); let mut noise = FastNoiseLite::new();
noise.frequency = frequency; noise.frequency = frequency;
noise.noise_type = fastnoise_lite::NoiseType::OpenSimplex2; noise.noise_type = fastnoise_lite::NoiseType::OpenSimplex2;
@@ -20,7 +19,6 @@ impl SimpleSurfaceGenerator {
Self { Self {
noise, noise,
surface_height, surface_height,
chunk_size,
} }
} }
@@ -30,20 +28,20 @@ impl SimpleSurfaceGenerator {
} }
} }
impl SurfaceGenerator for SimpleSurfaceGenerator { impl SimpleSurfaceGenerator {
fn generate(&self, column: &mut ChunkColumn) { fn generate(&self, column: &mut Chunk) {
let size_x = self.chunk_size.x; let size_x = CHUNK_SIZE;
let size_z = self.chunk_size.z; let size_z = CHUNK_SIZE;
let base_x = column.world_position.0 * size_x; let base_x = column.world_position.0 * size_x;
let base_z = column.world_position.1 * size_z; let base_z = column.world_position.1 * size_z;
let freq = 1.0 / 32.0; let freq = 1.0 / 32.0;
let column_height = self.chunk_size.y * self.surface_height as i32; let column_height = CHUNK_SIZE * self.surface_height as i32;
for x in 0..self.chunk_size.x { for x in 0..CHUNK_SIZE {
for z in 0..self.chunk_size.z { for z in 0..CHUNK_SIZE {
// world coordinates of this column // world coordinates of this column
let world_x = base_x + x; let world_x = base_x + x;
let world_z = base_z + z; let world_z = base_z + z;
@@ -71,7 +69,7 @@ impl SurfaceGenerator for SimpleSurfaceGenerator {
let noise = Self::normalize_value(noise); let noise = Self::normalize_value(noise);
let column_height = self.chunk_size.y * self.surface_height as i32; let column_height = CHUNK_SIZE * self.surface_height as i32;
let height = (noise.powi(2) * column_height as f32) as i32; let height = (noise.powi(2) * column_height as f32) as i32;
pos.y <= height pos.y <= height

View File

@@ -7,7 +7,7 @@ mod editor;
mod generation; mod generation;
mod meshing; mod meshing;
mod rendering; mod rendering;
mod terrain; mod runtime;
struct FastVoxel; struct FastVoxel;

View File

@@ -1,5 +1,5 @@
use crate::chunk::chunk::CHUNK_SIZE; use crate::chunk::chunk::CHUNK_SIZE;
use crate::chunk::{Chunk, ChunkMesh}; use crate::chunk::{SubChunk, ChunkMesh};
use crate::editor::voxel_registry::VoxelRegistry; use crate::editor::voxel_registry::VoxelRegistry;
use crate::meshing::Mesher; use crate::meshing::Mesher;
use godot::prelude::*; use godot::prelude::*;
@@ -20,7 +20,7 @@ impl BinaryGreedyMesher {
Self Self
} }
pub fn generate_mesh(&self, chunk: &Chunk, mesh: &mut ChunkMesh) { pub fn generate_mesh(&self, chunk: &SubChunk, mesh: &mut ChunkMesh) {
mesh.vertices.clear(); mesh.vertices.clear();
mesh.normals.clear(); mesh.normals.clear();
mesh.indices.clear(); mesh.indices.clear();
@@ -34,7 +34,7 @@ impl BinaryGreedyMesher {
self.mesh_face(chunk, mesh, 5); // +Z (forward) self.mesh_face(chunk, mesh, 5); // +Z (forward)
} }
fn mesh_face(&self, chunk: &Chunk, mesh: &mut ChunkMesh, face_dir: usize) { fn mesh_face(&self, chunk: &SubChunk, mesh: &mut ChunkMesh, face_dir: usize) {
let size = CHUNK_SIZE as usize; let size = CHUNK_SIZE as usize;
// For each slice perpendicular to the face direction // For each slice perpendicular to the face direction
@@ -191,7 +191,7 @@ impl BinaryGreedyMesher {
quad: GreedyQuad, quad: GreedyQuad,
face_dir: usize, face_dir: usize,
axis: usize, axis: usize,
chunk: &Chunk, chunk: &SubChunk,
) { ) {
let base_idx = mesh.vertices.len() as i32; let base_idx = mesh.vertices.len() as i32;
@@ -297,13 +297,13 @@ impl BinaryGreedyMesher {
} }
impl Mesher for BinaryGreedyMesher { impl Mesher for BinaryGreedyMesher {
fn generate_mesh(&self, chunk: &Chunk, mesh: &mut ChunkMesh) { fn generate_mesh(&self, chunk: &SubChunk, mesh: &mut ChunkMesh) {
self.generate_mesh(chunk, mesh); self.generate_mesh(chunk, mesh);
} }
fn generate_mesh_with_registry( fn generate_mesh_with_registry(
&self, &self,
chunk: &Chunk, chunk: &SubChunk,
_registry: &VoxelRegistry, _registry: &VoxelRegistry,
mesh: &mut ChunkMesh, mesh: &mut ChunkMesh,
) { ) {

View File

@@ -1,12 +1,12 @@
use crate::chunk::{Chunk, ChunkMesh}; use crate::chunk::{ChunkMesh, SubChunk};
use crate::editor::voxel_registry::VoxelRegistry; use crate::editor::voxel_registry::VoxelRegistry;
use godot::prelude::*; use godot::prelude::*;
pub trait Mesher: Send + Sync { pub trait Mesher: Send + Sync {
fn generate_mesh(&self, chunk: &Chunk, mesh: &mut ChunkMesh); fn generate_mesh(&self, chunk: &SubChunk, mesh: &mut ChunkMesh);
fn generate_mesh_with_registry( fn generate_mesh_with_registry(
&self, &self,
chunk: &Chunk, chunk: &SubChunk,
registry: &VoxelRegistry, registry: &VoxelRegistry,
mesh: &mut ChunkMesh, mesh: &mut ChunkMesh,
); );
@@ -24,7 +24,7 @@ impl CullingMesher {
impl Mesher for CullingMesher { impl Mesher for CullingMesher {
fn generate_mesh_with_registry( fn generate_mesh_with_registry(
&self, &self,
chunk: &Chunk, chunk: &SubChunk,
registry: &VoxelRegistry, registry: &VoxelRegistry,
mesh: &mut ChunkMesh, mesh: &mut ChunkMesh,
) { ) {
@@ -32,7 +32,7 @@ impl Mesher for CullingMesher {
self.generate_mesh(chunk, mesh); self.generate_mesh(chunk, mesh);
} }
fn generate_mesh(&self, chunk: &Chunk, mesh: &mut ChunkMesh) { fn generate_mesh(&self, chunk: &SubChunk, mesh: &mut ChunkMesh) {
mesh.clear(); mesh.clear();
// Simple CullingMesher - generate a quad for each solid voxel face // Simple CullingMesher - generate a quad for each solid voxel face
@@ -72,7 +72,7 @@ impl CullingMesher {
z: i32, z: i32,
voxel: bool, voxel: bool,
mesh: &mut ChunkMesh, mesh: &mut ChunkMesh,
chunk: &Chunk, chunk: &SubChunk,
) { ) {
let color = Color { let color = Color {
r: 255.0, r: 255.0,

View File

@@ -1,5 +1,5 @@
use crate::chunk::chunk::CHUNK_SIZE; use crate::chunk::chunk::CHUNK_SIZE;
use crate::chunk::{Chunk, ChunkMesh}; use crate::chunk::{SubChunk, ChunkMesh};
use crate::editor::voxel_registry::{VoxelModel, VoxelRegistry}; use crate::editor::voxel_registry::{VoxelModel, VoxelRegistry};
use crate::meshing::Mesher; use crate::meshing::Mesher;
use godot::prelude::*; use godot::prelude::*;
@@ -74,7 +74,7 @@ impl TexturedMesher {
] ]
} }
#[inline(always)] #[inline(always)]
fn is_face_visible(chunk: &Chunk, x: usize, y: usize, z: usize, face: usize) -> bool { fn is_face_visible(chunk: &SubChunk, x: usize, y: usize, z: usize, face: usize) -> bool {
// Convert to i32 for neighbor calculation // Convert to i32 for neighbor calculation
let (nx, ny, nz) = match face { let (nx, ny, nz) = match face {
0 => (x as i32 - 1, y as i32, z as i32), // LEFT 0 => (x as i32 - 1, y as i32, z as i32), // LEFT
@@ -108,7 +108,7 @@ impl TexturedMesher {
z: usize, z: usize,
model: &VoxelModel, // Already borrowed, no binding needed model: &VoxelModel, // Already borrowed, no binding needed
mesh: &mut ChunkMesh, mesh: &mut ChunkMesh,
chunk: &Chunk, chunk: &SubChunk,
) { ) {
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);
@@ -168,13 +168,13 @@ impl TexturedMesher {
} }
impl Mesher for TexturedMesher { impl Mesher for TexturedMesher {
fn generate_mesh(&self, chunk: &Chunk, mesh: &mut ChunkMesh) { fn generate_mesh(&self, chunk: &SubChunk, mesh: &mut ChunkMesh) {
panic!("TexturedMesher requires a VoxelRegistry. Use generate_mesh_with_registry instead."); panic!("TexturedMesher requires a VoxelRegistry. Use generate_mesh_with_registry instead.");
} }
fn generate_mesh_with_registry( fn generate_mesh_with_registry(
&self, &self,
chunk: &Chunk, chunk: &SubChunk,
registry: &VoxelRegistry, registry: &VoxelRegistry,
mesh: &mut ChunkMesh, mesh: &mut ChunkMesh,
) { ) {

View File

@@ -1,6 +1,6 @@
use std::collections::HashMap; use std::collections::HashMap;
use crate::chunk::{Chunk, ChunkMesh}; use crate::chunk::{ChunkMesh, SubChunk};
use godot::{ use godot::{
classes::{ classes::{
ArrayMesh, Material, MeshInstance3D, ResourceLoader, StandardMaterial3D, ArrayMesh, Material, MeshInstance3D, ResourceLoader, StandardMaterial3D,
@@ -40,7 +40,7 @@ impl Renderer {
.and_then(|resource| resource.try_cast::<Material>().ok()) .and_then(|resource| resource.try_cast::<Material>().ok())
} }
pub fn render_chunk(&mut self, chunk: &Chunk, mesh: &ChunkMesh) -> Gd<MeshInstance3D> { pub fn render_chunk(&mut self, chunk: &SubChunk, mesh: &ChunkMesh) -> Gd<MeshInstance3D> {
let chunk_key = chunk.world_position; 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); let chunk_key_i32 = (chunk_key.0 as i32, chunk_key.1 as i32, chunk_key.2 as i32);
@@ -137,7 +137,11 @@ impl Renderer {
mesh_instance mesh_instance
} }
pub fn render_chunk_with_uvs(&mut self, chunk: &Chunk, mesh: &ChunkMesh) -> Gd<MeshInstance3D> { pub fn render_chunk_with_uvs(
&mut self,
chunk: &SubChunk,
mesh: &ChunkMesh,
) -> Gd<MeshInstance3D> {
let chunk_key = chunk.world_position; 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); let chunk_key_i32 = (chunk_key.0 as i32, chunk_key.1 as i32, chunk_key.2 as i32);

5
src/runtime/mod.rs Normal file
View File

@@ -0,0 +1,5 @@
pub mod runtime;
pub mod types;
pub use runtime::Runtime;
pub use runtime::*;

View File

@@ -1,17 +1,17 @@
use std::collections::HashMap; use std::collections::HashMap;
use crate::{chunk::ChunkColumn, editor::world_config::WorldConfig, rendering::Renderer}; use crate::{chunk::Chunk, runtime::types::WorldConfig};
pub struct TerrainManager { pub struct Runtime {
chunks: HashMap<(i32, i32), ChunkColumn>, chunks: HashMap<(i32, i32), Chunk>,
// renderer: Renderer, // renderer: Renderer,
} }
impl TerrainManager { impl Runtime {
pub fn new(config: WorldConfig) -> Self { pub fn new(config: WorldConfig, registry: VoxelRegistry) -> Self {
// Terrain Size calculation // Terrain Size calculation
let width = config.get_render_distance() * 2 + 1; let width = config.render_distance * 2 + 1;
let height = config.get_terrain_height() as u8; let height = config.surface_height as u8;
let capacity = (width * width * height) as usize; let capacity = (width * width * height) as usize;
Self { Self {
@@ -28,7 +28,7 @@ impl TerrainManager {
* We preallocate this capacity to minimize HashMap reallocations as the * We preallocate this capacity to minimize HashMap reallocations as the
* visible terrain around the player is populated. * visible terrain around the player is populated.
***/ ***/
chunks: HashMap::with_capacity(capacity), chunks: HashMap::<(i32, i32), Chunk>::with_capacity(capacity),
} }
} }
} }

17
src/runtime/types.rs Normal file
View File

@@ -0,0 +1,17 @@
use crate::generation::generator::TerrainGenerator;
pub trait IntoRuntime<T> {
fn into_runtime(&self) -> T;
}
pub struct WorldConfig {
// ===== WORLD GROUP =====
pub render_distance: u8,
pub worker_threads: u8,
pub surface_height: u8,
// ===== GENERATION GROUP =====
generator: Box<dyn TerrainGenerator>,
}

View File

@@ -1,3 +0,0 @@
pub mod terrain_manager;
pub use terrain_manager::TerrainManager;