diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2e8df7a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 FastVoxel contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..ae7e8a7 --- /dev/null +++ b/README.md @@ -0,0 +1,384 @@ +# FastVoxel + +

+ FastVoxel logo +

+ +**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/GDExtension side of the project. + +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 + +- Rust-based Godot 4 GDExtension +- chunked voxel mesh pipeline +- bit-packed voxel storage (solid / air) +- procedural terrain using `fastnoise-lite` +- chunk streaming around the player +- runtime cube meshing with texture atlas support +- Godot editor resources for config + voxel registry + +## Soon: + +- Multi-Threaded chunk meshing and world generation using a `Work Stealing` Thread pool with divide‑and‑conquer parallelism. + +## Screenshots + +

+ Albedo Vertex color +

+ +

+ Heightmap Surface Noise +

+ +

+ Albedo Texture + Normal map +

+ +Right now the repo only contains branding assets. I haven't added gameplay screenshots yet. + +If you want to add screenshots that show directly on the README, the easiest thing is to just drop images somewhere like: + +``` +docs/screenshots/WHATEVER.png +``` + +Then embed them in the README like: + +```md +![WHATEVER](docs/screenshots/WHATEVER.png) +``` + +## What FastVoxel Actually Does + +The engine generates a voxel world using chunk columns. + +Rough pipeline looks like this: + +1. A `SurfaceGenerator` decides if a voxel is solid or not. +2. A `ChunkColumn` holds a vertical stack of chunks. +3. Each `Chunk` stores voxel occupancy in a compact bit-packed format. +4. A `Mesher` converts visible voxel faces into triangle meshes. +5. A `Renderer` turns those meshes into Godot `MeshInstance3D` nodes. +6. A world node updates loaded terrain as the player moves around. + +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 + +### Chunked world layout + +Terrain is divided into columns of chunks. + +Current defaults: + +- `CHUNK_SIZE = 32` +- each chunk = `32 × 32 × 32` voxels +- columns stack multiple chunks vertically like a hamburger +- chunk loading/unloading happens around the player based on render distance + +Relevant files: + +- `src/chunk/chunk.rs` +- `src/chunk/column.rs` +- `src/chunk/chunk_manager.rs` + +### Bit-packed voxel storage + +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`. + +right now a voxel is basically: + +- `0` -> air +- `1` -> solid + +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. + +### Surface generation + +The default generator is `SimpleSurfaceGenerator`. + +It uses `fastnoise-lite` to create a heightmap and then fills voxels from the bottom up to that height. + +Important files: + +- `src/generation/generator.rs` +- `src/generation/simple_heightmap.rs` + +The generation system is trait-based so other terrain approaches can be plugged in later. + +### Meshing + +There are a few different meshing implementations right now. + +- `CullingMesher` – basic visible-face meshing +- `TexturedMesher` – cube meshing with texture atlas support +- `BinaryGreedyMesher` – more optimization-focused experiment + +Relevant files: + +- `src/meshing/mesher.rs` +- `src/meshing/textured_mesher.rs` +- `src/meshing/binary_greedy_mesher.rs` + +The textured mesher is currently the most useful one if you're aiming for something Minecraft-like since it supports per-face UV lookup via a voxel registry. + +### Godot integration + +The engine exposes a few classes/resources to Godot via GDExtension. + +Main ones: + +- `WorldConfig` – terrain + generation settings +- `VoxelRegistry` – describes voxel types and atlas tiles +- `World` – runtime node responsible for generation and updates +- `WorldPlugin` – editor-side plugin hooks + +Files: + +- `src/editor/world_config.rs` +- `src/editor/voxel_registry.rs` +- `src/editor/world_node.rs` +- `src/editor/world_plugin.rs` + +## Project Structure + +``` +src/ +├── chunk/ # chunk data, columns, chunk manager, mesh buffers +├── editor/ # Godot resources, world node, editor plugin +├── generation/ # generator traits + procedural terrain generation +├── meshing/ # meshing strategies +├── rendering/ # converting mesh data to Godot meshes +├── terrain/ # higher-level terrain experiments / refactor work +└── voxel/ # voxel registry + earlier voxel abstractions +``` + +## Build and Install + +### Requirements + +You need: + +- Rust toolchain +- a Godot 4 project set up for GDExtension +- optionally a sibling Godot project if you want to use the provided `build.sh` + +### Build the extension + +``` +cargo build --release +``` + +### Helper build script + +There's a small `build.sh` script that builds the Rust library and copies it into a Godot project. + +By default it expects a project at: + +``` +../minekoloft +``` + +It copies the Linux shared library to: + +``` +../minekoloft/addons/fastvoxel/bin/linux +``` + +Run it with: + +``` +./build.sh +``` + +If your Godot project is somewhere else, just edit the `GODOT_PROJECT` path in the script. + +## Using It in Godot + +Typical workflow looks like this: + +1. build the Rust extension +2. copy/install the addon into your Godot project +3. create a `WorldConfig` resource +4. create a `VoxelRegistry` resource +5. add a `World` node to a scene +6. assign the config and registry in the inspector +7. trigger terrain generation +8. call the update method each frame with the player position + +### WorldConfig + +`WorldConfig` stores generation settings like: + +- `chunk_size` +- `render_distance` +- `worker_threads` +- `seed` +- `frequency` +- `terrain_height` + +Default values in the code are roughly: + +- chunk size: `Vector3i(32, 32, 32)` +- render distance: `8` +- worker threads: `4` +- seed: `1234` +- frequency: `0.03` +- terrain height: `6` + +These are just meant for quick test worlds. + +### VoxelRegistry + +`VoxelRegistry` is a Godot `Resource` containing voxel model definitions. + +Each `VoxelModel` can specify: + +- voxel type +- atlas size +- tile index for each cube face + +Faces supported: + +- left +- right +- bottom +- top +- back +- front + +The mesher uses this to assign UVs when building meshes. + +### Runtime API + +The intended `World` node API currently includes: + +- `generate_terrain()` +- `regenerate_terrain()` +- `clear_terrain()` +- `update_from_gdscript(player_world_pos: Vector3)` + +Example usage in GDScript: + +```gdscript +@onready var world = $World +@onready var player = $Player + +func _ready() -> void: + world.generate_terrain() + +func _process(_delta: float) -> void: + world.update_from_gdscript(player.global_position) +``` + +## Engine-Level API Notes + +### Generation trait + +Terrain generation is abstracted behind `SurfaceGenerator`. + +```rust +pub trait SurfaceGenerator: Send + Sync { + fn generate(&self, column: &mut ChunkColumn); + fn sample_voxel(&self, pos: Vector3i) -> bool; +} +``` + +This makes it easier to experiment with: + +- different noise algorithms +- caves +- biome systems +- deterministic seeds +- threaded generation later on + +### Mesher trait + +Meshing also uses a trait interface. + +```rust +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; +} +``` + +Some meshers only need occupancy data, while others need material/registry info. + +Keeping this separated makes it easier to experiment with meshing strategies without touching world management code. + +### ChunkManager + +`ChunkManager` is responsible for most of the runtime work: + +- managing chunk column lifetimes +- generating terrain around the player +- unloading distant chunks +- triggering meshing +- submitting meshes to the renderer +- handing mesh instances back to the scene tree + +So it basically sits between generation, meshing, and rendering. + +## Current State + +The project is mid-development and some systems are being refactored (mainly around the world/terrain layers). + +But the overall direction is pretty clear: + +- compact chunk storage +- trait-based generation +- pluggable meshing +- Godot editor resources +- runtime chunk streaming + +## Why This README Exists + +Mostly so the repo has an actual front page explaining: + +- what the project is +- how it works +- how to build it +- where to start reading the code + +Wiki pages tend to be less visible when someone first lands on the repository. + +## Possible Future Work + +Some things likely coming next: + +- finishing the current terrain/world refactor +- adding real gameplay screenshots or gifs +- stabilizing the Godot-facing API +- multithreaded chunk generation **(GOD HELP)** +- more voxel/material data in storage +- improved greedy meshing and cross-chunk face handling +- a small example Godot project using the addon + +## License + +This project is licensed under the MIT License. + +See the `LICENSE` file for the full text. diff --git a/build.sh b/build.sh index 1f70ed1..adf3661 100755 --- a/build.sh +++ b/build.sh @@ -17,7 +17,7 @@ TMP="$DEST_LINUX/lib${LIB_NAME}.so.tmp" cp "target/release/lib${LIB_NAME}.so" "$TMP" mv "$TMP" "$DEST_LINUX/lib${LIB_NAME}.so" -# Optional: Cross‑compile for Windows (if needed) +# Optional: Cross compile for Windows (if needed) # Uncomment the following lines if you have the Windows target installed # echo "Building for Windows (cross-compile)..." # cargo build --release --target x86_64-pc-windows-gnu diff --git a/docs/screenshots/colors.png b/docs/screenshots/colors.png new file mode 100644 index 0000000..6a4bb3d Binary files /dev/null and b/docs/screenshots/colors.png differ diff --git a/docs/screenshots/heightmap-noise.png b/docs/screenshots/heightmap-noise.png new file mode 100644 index 0000000..d965e8c Binary files /dev/null and b/docs/screenshots/heightmap-noise.png differ diff --git a/docs/screenshots/normal-map.png b/docs/screenshots/normal-map.png new file mode 100644 index 0000000..d5f8c56 Binary files /dev/null and b/docs/screenshots/normal-map.png differ diff --git a/src/chunk/chunk.rs b/src/chunk/chunk.rs index ae3eff1..ffa1005 100644 --- a/src/chunk/chunk.rs +++ b/src/chunk/chunk.rs @@ -2,11 +2,12 @@ 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 { +pub struct SubChunk { // Bitpacked voxel data: 0 = air, 1 = solid pub voxels: Vec, // WORLD COORDINATES @@ -14,10 +15,10 @@ pub struct Chunk { modified: bool, } -impl Chunk { +impl SubChunk { /// 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 { - Chunk { + SubChunk { voxels: vec![0u32; BITPACKED_SIZE], world_position: (world_pos_x, world_pos_y, world_pos_z), modified: false, @@ -30,7 +31,7 @@ impl Chunk { // as usize // } #[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 } @@ -52,7 +53,7 @@ impl Chunk { local_pos ); - let index = Self::get_voxel_index(local_pos, chunk_size); + let index = Self::get_voxel_index(local_pos); debug_assert!( index < BITPACKED_SIZE, "Voxel index {:?} out of chunk bounds", @@ -71,7 +72,7 @@ impl Chunk { "Voxel position {:?} out of chunk bounds", voxel_pos ); - let index = Self::get_voxel_index(voxel_pos, chunk_size); + let index = Self::get_voxel_index(voxel_pos); let word_index = index >> 5; let bit_index = index & 31; diff --git a/src/chunk/chunk_manager.rs b/src/chunk/chunk_manager.rs index 54f2f4d..d433550 100644 --- a/src/chunk/chunk_manager.rs +++ b/src/chunk/chunk_manager.rs @@ -3,17 +3,16 @@ 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::chunk::{Chunk, ChunkMesh, SubChunk}; use crate::editor::voxel_registry::VoxelRegistry; -use crate::generation::SimpleSurfaceGenerator; -use crate::generation::generator::SurfaceGenerator; +use crate::generation::generator::TerrainGenerator; 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 chunk_columns: HashMap<(i32, i32), Chunk>, pub renderer: Renderer, pub last_player_chunk: (i32, i32), @@ -43,7 +42,7 @@ impl ChunkManager { player_world_pos: Vector3, render_distance: i32, registry: &VoxelRegistry, - surface_generator: &dyn SurfaceGenerator, + surface_generator: &dyn TerrainGenerator, mesher: &dyn Mesher, ) -> bool { 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, distance: i32, registry: &VoxelRegistry, - generator: &dyn SurfaceGenerator, + generator: &dyn TerrainGenerator, mesher: &dyn Mesher, ) { use std::collections::HashSet; @@ -129,21 +128,21 @@ impl ChunkManager { index_x: i32, index_z: i32, registry: &VoxelRegistry, - generator: &dyn SurfaceGenerator, + generator: &dyn TerrainGenerator, 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(); - generator.generate(&mut column); + generator.sample_chunk(&mut column); 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) { + if let Some(chunk) = column.get_sub_chunk(i as usize) { self.mesh_and_render_chunk(chunk, mesher, registry); } } @@ -154,7 +153,7 @@ impl ChunkManager { fn mesh_and_render_chunk( &mut self, - chunk: &Chunk, + chunk: &SubChunk, mesher: &dyn Mesher, registry: &VoxelRegistry, ) { diff --git a/src/chunk/column.rs b/src/chunk/column.rs index 862bed7..6670709 100644 --- a/src/chunk/column.rs +++ b/src/chunk/column.rs @@ -1,26 +1,31 @@ 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 struct ChunkColumn { - pub chunks: [Option; CHUNKS_PER_COLUMN], +pub struct ChunkPos { + pub x: i64, + pub y: i64, +} + +pub struct Chunk { + pub sub_chunks: [Option; CHUNKS_PER_COLUMN], pub world_position: (i32, i32), // XZ pub chunk_size: Vector3i, } -impl ChunkColumn { +impl Chunk { pub fn new(x: i32, z: i32, chunk_size: Vector3i) -> Self { - ChunkColumn { - chunks: [None, None, None, None, None, None, None, None], + Chunk { + sub_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 { + pub fn get_sub_chunk_index(chunk_size_y: i32, world_y: i32) -> usize { (world_y / chunk_size_y) as usize } @@ -34,31 +39,31 @@ impl ChunkColumn { (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() { + pub fn get_or_create_sub_chunk(&mut self, chunk_y_index: i32) -> &mut SubChunk { + if self.sub_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( + self.sub_chunks[chunk_y_index as usize] = Some(SubChunk::new( world_x as f64, (chunk_y_index) 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> { - self.chunks[chunk_y_index].as_ref() + pub fn get_sub_chunk(&self, chunk_y_index: usize) -> Option<&SubChunk> { + self.sub_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 get_sub_chunk_mut(&mut self, chunk_y_index: usize) -> Option<&mut SubChunk> { + self.sub_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); + let chunk_index = Self::get_sub_chunk_index(self.chunk_size.y, world_pos.y); if chunk_index < CHUNKS_PER_COLUMN { 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_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 { - let chunk_index = Self::get_chunk_index(self.chunk_size.y, world_pos.y); - if let Some(chunk) = self.get_chunk(chunk_index) { + let chunk_index = Self::get_sub_chunk_index(self.chunk_size.y, world_pos.y); + if let Some(chunk) = self.get_sub_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); diff --git a/src/chunk/mod.rs b/src/chunk/mod.rs index e814609..c00901c 100644 --- a/src/chunk/mod.rs +++ b/src/chunk/mod.rs @@ -3,7 +3,7 @@ pub mod chunk_manager; pub mod column; pub mod mesh; -pub use chunk::Chunk; +pub use chunk::SubChunk; pub use chunk_manager::ChunkManager; -pub use column::ChunkColumn; +pub use column::Chunk; pub use mesh::ChunkMesh; diff --git a/src/editor/mod.rs b/src/editor/mod.rs index cf130f8..5e1397d 100644 --- a/src/editor/mod.rs +++ b/src/editor/mod.rs @@ -1,4 +1,7 @@ -pub mod editor; pub mod voxel_registry; +pub mod world_config; +pub mod world_generator; +pub mod world_node; +pub mod world_plugin; pub use voxel_registry::VoxelRegistry; diff --git a/src/editor/voxel_registry.rs b/src/editor/voxel_registry.rs index 1a798ce..af84ea3 100644 --- a/src/editor/voxel_registry.rs +++ b/src/editor/voxel_registry.rs @@ -7,7 +7,7 @@ use godot::{ prelude::{Export, GodotClass, GodotConvert, Var}, }; -#[derive(GodotConvert, Var, Export, Debug, Clone, Copy)] +#[derive(GodotConvert, Var, Export, Debug, Clone, Copy, PartialEq)] #[godot(via = i64)] pub enum VoxelType { Empty, @@ -58,26 +58,17 @@ pub struct VoxelModel { impl VoxelModel { #[inline] pub fn is_solid(&self) -> bool { - match self.voxel_type { - VoxelType::Empty => false, - _ => true, - } + self.voxel_type != VoxelType::Empty } #[inline] pub fn is_empty(&self) -> bool { - match self.voxel_type { - VoxelType::Empty => true, - _ => false, - } + self.voxel_type == VoxelType::Empty } #[inline] pub fn is_cube(&self) -> bool { - match self.voxel_type { - VoxelType::Cube => true, - _ => false, - } + self.voxel_type == VoxelType::Cube } #[inline] diff --git a/src/editor/world_config.rs b/src/editor/world_config.rs new file mode 100644 index 0000000..5bf0e43 --- /dev/null +++ b/src/editor/world_config.rs @@ -0,0 +1,44 @@ +use godot::{ + classes::{IResource, Resource}, + obj::{Base, Gd}, + prelude::{GodotClass, godot_api}, +}; + +use crate::editor::world_generator::WorldGeneratorResource; + +#[derive(GodotClass)] +#[class(base=Resource)] +pub struct WorldConfig { + base: Base, + + // ===== WORLD GROUP ===== + #[export_group(name = "World")] + #[export(range = (2.0,64.0))] + render_distance: u8, + + #[export(range = (1.0, 64.0, 1.0))] + worker_threads: u8, + + // ===== GENERATION GROUP ===== + #[export_group(name = "Generation")] + #[export] + generator: Option>, + + #[export(range = (1.0, 10.0))] + surface_height: u8, +} + +#[godot_api] +impl IResource for WorldConfig { + fn init(base: Base) -> Self { + // DEFAULT Values + Self { + base, + render_distance: 8, + worker_threads: 8, + generator: None, + // noise: None, + surface_height: 6, + } + } +} diff --git a/src/editor/world_generator.rs b/src/editor/world_generator.rs new file mode 100644 index 0000000..92f25c9 --- /dev/null +++ b/src/editor/world_generator.rs @@ -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, + + #[export] + pub noise2d: Option>, +} +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 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(), + } + } +} diff --git a/src/editor/world_node.rs b/src/editor/world_node.rs new file mode 100644 index 0000000..1b5fdfd --- /dev/null +++ b/src/editor/world_node.rs @@ -0,0 +1,153 @@ +use godot::classes::INode3D; +use godot::prelude::*; + +use crate::editor::VoxelRegistry; +use crate::editor::world_config::WorldConfig; +use crate::runtime::Runtime; + +#[derive(GodotClass)] +#[class(base=Node3D,tool)] +pub struct World { + base: Base, + + #[export_group(name = "Config")] + + /// World generation and rendering settings + #[export] + config: Option>, + + #[export_group(name = "Voxel Registry")] + #[export] + registry: Option>, + + state: WorldState, +} + +pub enum WorldState { + Uninitialized, + Ready { runtime: Runtime }, +} + +#[godot_api] +impl INode3D for World { + fn init(base: Base) -> Self { + godot_print!("🧊 FastVoxel Init..."); + Self { + base, + config: None, + registry: None, + state: WorldState::Uninitialized, + } + } + + fn process(&mut self, _delta: f64) { + // self.add_pending_mesh_instances_to_scene(); + } + + fn ready(&mut self) { + self.initialize_runtime(); + } +} + +impl World { + fn initialize_runtime(&mut self) { + match (&self.registry, &self.config) { + (Some(reg), Some(cfg)) => { + let cfg_ref = cfg.bind(); + let reg_ref = reg.bind(); + let runtime = Runtime::new(cfg_ref, reg_ref); + self.state = WorldState::Ready { runtime }; + } + _ => { + godot_error!("World: missing resources (config or registry)."); + self.state = WorldState::Uninitialized; + } + } + } +} + +/*** + * Public API is below here. function that can be called by GDScript + * or other gdextensions in-order to manage and do all sorts of things +***/ +#[godot_api] +impl World { + #[func] + fn generate_world(&mut self, center_pos: Vector3i) {} + // #[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 index_x in -distance..=distance { + // for index_z in -distance..=distance { + // self.chunk_manager.generate_chunk_column( + // index_x, + // index_z, + // &library_ref, + // self.surface_generator.as_ref(), + // self.mesher.as_ref(), + // ); + // } + // } + // godot_print!("Textured terrain regenerated!"); + // } + // } + // } + + // #[func] + // fn regenerate_terrain(&mut self) { + // godot_print!("Regenerating terrain..."); + + // self.chunk_manager.clear(); + + // // Create textured mesher + // self.mesher = Box::new(TexturedMesher::new()); + + // self.generate_terrain(); + // } + + // #[func] + // fn clear_terrain(&mut self) { + // godot_print!("Clearing Terrain..."); + + // self.chunk_manager.clear(); + + // self.chunk_manager.pending_mesh_instances.clear(); + // self.chunk_manager.renderer.clear_and_destroy(); + // } + + // #[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, + // ®istry_ref, + // self.surface_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::()); + // } + // } +} diff --git a/src/editor/editor.rs b/src/editor/world_plugin.rs similarity index 95% rename from src/editor/editor.rs rename to src/editor/world_plugin.rs index 11d752f..624b911 100644 --- a/src/editor/editor.rs +++ b/src/editor/world_plugin.rs @@ -8,7 +8,7 @@ use godot::classes::Texture2D; use godot::classes::editor_plugin::CustomControlContainer; use godot::prelude::*; -use crate::world::World; +use crate::editor::world_node::World; #[derive(GodotClass)] #[class(init, base=EditorPlugin, tool)] @@ -93,6 +93,11 @@ impl IEditorPlugin for WorldPlugin { // --- Build the dropdown menu --- let mut popup = menu_button.get_popup(); // Returns a PopupMenu + popup + .as_mut() + .expect("no popup") + .add_check_item("Follow Editor Camera"); + popup.as_mut().expect("no popup").add_separator(); popup.as_mut().expect("no popup").add_item("Rebuild World"); // item text, id popup.as_mut().expect("no popup").add_item("Clear World"); popup.as_mut().expect("no popup").add_item("Reload Chunks"); diff --git a/src/generation/generator.rs b/src/generation/generator.rs index 8a33d9b..46f5cb5 100644 --- a/src/generation/generator.rs +++ b/src/generation/generator.rs @@ -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 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; -} - -pub trait SurfaceGenerator: Send + Sync { - fn generate(&self, column: &mut ChunkColumn); - fn sample_voxel(&self, pos: Vector3i) -> bool; +pub trait TerrainGenerator: Send + Sync { + fn sample_chunk(&self, column: &mut Chunk); + fn sample_height(&self, x: f32, z: f32) -> f32; } diff --git a/src/generation/mod.rs b/src/generation/mod.rs index ffcfb21..2c4dc6d 100644 --- a/src/generation/mod.rs +++ b/src/generation/mod.rs @@ -1,5 +1,4 @@ pub mod generator; pub mod simple_heightmap; -pub use generator::WorldGenerator; pub use simple_heightmap::SimpleSurfaceGenerator; diff --git a/src/generation/simple_heightmap.rs b/src/generation/simple_heightmap.rs index 4aab94f..0ab1b23 100644 --- a/src/generation/simple_heightmap.rs +++ b/src/generation/simple_heightmap.rs @@ -1,16 +1,15 @@ 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 { noise: FastNoiseLite, surface_height: u32, - chunk_size: Vector3i, } 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(); noise.frequency = frequency; noise.noise_type = fastnoise_lite::NoiseType::OpenSimplex2; @@ -20,7 +19,6 @@ impl SimpleSurfaceGenerator { Self { noise, surface_height, - chunk_size, } } @@ -30,20 +28,20 @@ impl SimpleSurfaceGenerator { } } -impl SurfaceGenerator for SimpleSurfaceGenerator { - fn generate(&self, column: &mut ChunkColumn) { - let size_x = self.chunk_size.x; - let size_z = self.chunk_size.z; +impl SimpleSurfaceGenerator { + fn generate(&self, column: &mut Chunk) { + let size_x = CHUNK_SIZE; + let size_z = CHUNK_SIZE; let base_x = column.world_position.0 * size_x; let base_z = column.world_position.1 * size_z; let freq = 1.0 / 32.0; - let column_height = self.chunk_size.y * self.surface_height as i32; + let column_height = CHUNK_SIZE * self.surface_height as i32; - for x in 0..self.chunk_size.x { - for z in 0..self.chunk_size.z { + for x in 0..CHUNK_SIZE { + for z in 0..CHUNK_SIZE { // world coordinates of this column let world_x = base_x + x; let world_z = base_z + z; @@ -71,7 +69,7 @@ impl SurfaceGenerator for SimpleSurfaceGenerator { 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; pos.y <= height diff --git a/src/lib.rs b/src/lib.rs index 5f2e340..2a0be0a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,13 +1,13 @@ use godot::prelude::*; mod voxel; -mod world; mod chunk; mod editor; mod generation; mod meshing; mod rendering; +mod runtime; struct FastVoxel; diff --git a/src/meshing/binary_greedy_mesher.rs b/src/meshing/binary_greedy_mesher.rs index ef5e39c..2985d68 100644 --- a/src/meshing/binary_greedy_mesher.rs +++ b/src/meshing/binary_greedy_mesher.rs @@ -1,5 +1,5 @@ use crate::chunk::chunk::CHUNK_SIZE; -use crate::chunk::{Chunk, ChunkMesh}; +use crate::chunk::{SubChunk, ChunkMesh}; use crate::editor::voxel_registry::VoxelRegistry; use crate::meshing::Mesher; use godot::prelude::*; @@ -20,7 +20,7 @@ impl BinaryGreedyMesher { 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.normals.clear(); mesh.indices.clear(); @@ -34,7 +34,7 @@ impl BinaryGreedyMesher { 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; // For each slice perpendicular to the face direction @@ -191,7 +191,7 @@ impl BinaryGreedyMesher { quad: GreedyQuad, face_dir: usize, axis: usize, - chunk: &Chunk, + chunk: &SubChunk, ) { let base_idx = mesh.vertices.len() as i32; @@ -297,13 +297,13 @@ impl 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); } fn generate_mesh_with_registry( &self, - chunk: &Chunk, + chunk: &SubChunk, _registry: &VoxelRegistry, mesh: &mut ChunkMesh, ) { diff --git a/src/meshing/mesher.rs b/src/meshing/mesher.rs index a18e9ca..fc8dd6e 100644 --- a/src/meshing/mesher.rs +++ b/src/meshing/mesher.rs @@ -1,12 +1,12 @@ -use crate::chunk::{Chunk, ChunkMesh}; +use crate::chunk::{ChunkMesh, SubChunk}; 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(&self, chunk: &SubChunk, mesh: &mut ChunkMesh); fn generate_mesh_with_registry( &self, - chunk: &Chunk, + chunk: &SubChunk, registry: &VoxelRegistry, mesh: &mut ChunkMesh, ); @@ -24,7 +24,7 @@ impl CullingMesher { impl Mesher for CullingMesher { fn generate_mesh_with_registry( &self, - chunk: &Chunk, + chunk: &SubChunk, registry: &VoxelRegistry, mesh: &mut ChunkMesh, ) { @@ -32,7 +32,7 @@ impl Mesher for CullingMesher { 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(); // Simple CullingMesher - generate a quad for each solid voxel face @@ -72,7 +72,7 @@ impl CullingMesher { z: i32, voxel: bool, mesh: &mut ChunkMesh, - chunk: &Chunk, + chunk: &SubChunk, ) { let color = Color { r: 255.0, diff --git a/src/meshing/textured_mesher.rs b/src/meshing/textured_mesher.rs index e553e95..423eced 100644 --- a/src/meshing/textured_mesher.rs +++ b/src/meshing/textured_mesher.rs @@ -1,5 +1,5 @@ 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::meshing::Mesher; use godot::prelude::*; @@ -74,7 +74,7 @@ impl TexturedMesher { ] } #[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 let (nx, ny, nz) = match face { 0 => (x as i32 - 1, y as i32, z as i32), // LEFT @@ -108,7 +108,7 @@ impl TexturedMesher { z: usize, model: &VoxelModel, // Already borrowed, no binding needed mesh: &mut ChunkMesh, - chunk: &Chunk, + chunk: &SubChunk, ) { let pos = Vector3::new(x as f32, y as f32, z as f32); @@ -168,13 +168,13 @@ impl 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."); } fn generate_mesh_with_registry( &self, - chunk: &Chunk, + chunk: &SubChunk, registry: &VoxelRegistry, mesh: &mut ChunkMesh, ) { diff --git a/src/rendering/renderer.rs b/src/rendering/renderer.rs index 907357a..f1caea3 100644 --- a/src/rendering/renderer.rs +++ b/src/rendering/renderer.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use crate::chunk::{Chunk, ChunkMesh}; +use crate::chunk::{ChunkMesh, SubChunk}; use godot::{ classes::{ ArrayMesh, Material, MeshInstance3D, ResourceLoader, StandardMaterial3D, @@ -40,7 +40,7 @@ impl Renderer { .and_then(|resource| resource.try_cast::().ok()) } - pub fn render_chunk(&mut self, chunk: &Chunk, mesh: &ChunkMesh) -> Gd { + pub fn render_chunk(&mut self, chunk: &SubChunk, mesh: &ChunkMesh) -> Gd { 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); @@ -137,7 +137,11 @@ impl Renderer { mesh_instance } - pub fn render_chunk_with_uvs(&mut self, chunk: &Chunk, mesh: &ChunkMesh) -> Gd { + pub fn render_chunk_with_uvs( + &mut self, + chunk: &SubChunk, + mesh: &ChunkMesh, + ) -> Gd { 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); diff --git a/src/runtime/mod.rs b/src/runtime/mod.rs new file mode 100644 index 0000000..e5c60d6 --- /dev/null +++ b/src/runtime/mod.rs @@ -0,0 +1,5 @@ +pub mod runtime; +pub mod types; + +pub use runtime::Runtime; +pub use runtime::*; diff --git a/src/runtime/runtime.rs b/src/runtime/runtime.rs new file mode 100644 index 0000000..f24830c --- /dev/null +++ b/src/runtime/runtime.rs @@ -0,0 +1,34 @@ +use std::collections::HashMap; + +use crate::{chunk::Chunk, runtime::types::WorldConfig}; + +pub struct Runtime { + chunks: HashMap<(i32, i32), Chunk>, + // renderer: Renderer, +} + +impl Runtime { + pub fn new(config: WorldConfig, registry: VoxelRegistry) -> Self { + // Terrain Size calculation + let width = config.render_distance * 2 + 1; + let height = config.surface_height as u8; + let capacity = (width * width * height) as usize; + + Self { + /*** + * The render distance defines a square area around the player in chunks. + * + * Example: + * With a render distance of 8, the player stands on the "center" chunk. + * There are 8 chunks in extended in each direction (left/right and forward/backward), + * forming a (8 * 2 + 1) = 17 chunk wide grid. + * + * Total chunks = (render_distance * 2 + 1).pow(2) * height + * + * We preallocate this capacity to minimize HashMap reallocations as the + * visible terrain around the player is populated. + ***/ + chunks: HashMap::<(i32, i32), Chunk>::with_capacity(capacity), + } + } +} diff --git a/src/runtime/types.rs b/src/runtime/types.rs new file mode 100644 index 0000000..7a60158 --- /dev/null +++ b/src/runtime/types.rs @@ -0,0 +1,17 @@ +use crate::generation::generator::TerrainGenerator; + +pub trait IntoRuntime { + 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, +} diff --git a/src/world.rs b/src/world.rs deleted file mode 100644 index 131b633..0000000 --- a/src/world.rs +++ /dev/null @@ -1,197 +0,0 @@ -use godot::classes::{INode3D, VoxelGi}; -use godot::prelude::*; - -use crate::chunk::ChunkManager; -use crate::editor::VoxelRegistry; -use crate::generation::SimpleSurfaceGenerator; -use crate::generation::generator::SurfaceGenerator; -use crate::meshing::binary_greedy_mesher::BinaryGreedyMesher; -use crate::meshing::{Mesher, TexturedMesher}; - -#[derive(GodotClass)] -#[class(base=Node3D,tool)] -pub struct World { - base: Base, - - // ===== 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>, - - #[export_group(name = "GI")] - #[export] - voxelgi_node: Option>, - - // World data (temporary) - chunk_manager: ChunkManager, - surface_generator: Box, - mesher: Box, -} - -#[godot_api] -impl INode3D for World { - fn init(base: Base) -> Self { - godot_print!("🧊 Hello from FastVoxel"); - - let surface_generator = Box::new(SimpleSurfaceGenerator::new( - 1234, - 0.3, - 6, - Vector3i { - x: 32, - y: 32, - z: 32, - }, - )); - - let mesher = Box::new(TexturedMesher::new()); - let chunk_manager = ChunkManager::new(); - - Self { - render_distance: 8, - chunk_size: Vector3i::new(16, 16, 16), - world_seed: 1234, - noise_frequency: 0.03, - terrain_height: 8, - base, - surface_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; - } - - let surface_generator = Box::new(SimpleSurfaceGenerator::new( - self.world_seed, - self.noise_frequency, - self.terrain_height, - self.chunk_size, - )); - self.surface_generator = surface_generator; - self.chunk_manager.chunk_size = self.chunk_size; - self.chunk_manager.terrain_height = self.terrain_height; - - 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 index_x in -distance..=distance { - for index_z in -distance..=distance { - self.chunk_manager.generate_chunk_column( - index_x, - index_z, - &library_ref, - self.surface_generator.as_ref(), - self.mesher.as_ref(), - ); - } - } - godot_print!("Textured terrain regenerated!"); - } - } - } - - #[func] - fn regenerate_terrain(&mut self) { - godot_print!("Regenerating terrain..."); - - self.chunk_manager.clear(); - - // Create textured mesher - self.mesher = Box::new(TexturedMesher::new()); - - self.generate_terrain(); - } - - #[func] - fn clear_terrain(&mut self) { - godot_print!("Clearing Terrain..."); - - self.chunk_manager.clear(); - - self.chunk_manager.pending_mesh_instances.clear(); - self.chunk_manager.renderer.clear_and_destroy(); - } - - #[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, - ®istry_ref, - self.surface_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::()); - } - } -} diff --git a/src/world/world_manager.rs b/src/world/world_manager.rs deleted file mode 100644 index 8ce3121..0000000 --- a/src/world/world_manager.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub struct WorldManager { - chunks: HashMap<(i32, i32), Chunk>, -}