From d2d678de4ad9c00cfe9a0a2e972baa217d66bce4 Mon Sep 17 00:00:00 2001 From: TheRealKaamy Date: Fri, 20 Mar 2026 18:37:38 +0330 Subject: [PATCH] re-working editor resource, chunk_manager and more. --- LICENSE | 21 ++ README.md | 362 ++++++++++++++++++++++ build.sh | 2 +- src/editor/mod.rs | 4 +- src/editor/voxel_registry.rs | 17 +- src/editor/world_config.rs | 49 +++ src/{world.rs => editor/world_node.rs} | 100 +++--- src/editor/{editor.rs => world_plugin.rs} | 7 +- src/lib.rs | 2 +- src/terrain/mod.rs | 3 + src/terrain/terrain_manager.rs | 34 ++ src/world/world_manager.rs | 3 - 12 files changed, 523 insertions(+), 81 deletions(-) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 src/editor/world_config.rs rename src/{world.rs => editor/world_node.rs} (70%) rename src/editor/{editor.rs => world_plugin.rs} (95%) create mode 100644 src/terrain/mod.rs create mode 100644 src/terrain/terrain_manager.rs delete mode 100644 src/world/world_manager.rs 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..cdf4a2b --- /dev/null +++ b/README.md @@ -0,0 +1,362 @@ +# FastVoxel + +

+ FastVoxel logo +

+ +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. + +This repo contains the engine/plugin 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. + +## Highlights + +- Rust-based Godot 4 GDExtension +- chunked voxel terrain 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 + +## Screenshots + +Right now the repo only contains branding assets. I haven't added gameplay screenshots yet. + +

+ FastVoxel mark +

+ +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. + +## 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 +- 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. + +So right now a voxel is basically: + +- `0` → air +- `1` → solid + +This keeps memory usage low and makes lookups very cheap. It works well for early terrain prototypes and simple block worlds. + +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/src/editor/mod.rs b/src/editor/mod.rs index cf130f8..63a0f15 100644 --- a/src/editor/mod.rs +++ b/src/editor/mod.rs @@ -1,4 +1,6 @@ -pub mod editor; pub mod voxel_registry; +pub mod world_config; +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..cf8e17b --- /dev/null +++ b/src/editor/world_config.rs @@ -0,0 +1,49 @@ +use godot::{ + classes::{IResource, Resource, class_macros::private::virtuals::Os::Vector3i}, + obj::Base, + prelude::{GodotClass, godot_api}, +}; + +#[derive(GodotClass)] +#[class(base=Resource)] +pub struct WorldConfig { + base: Base, + + // ===== WORLD GROUP ===== + #[export_group(name = "World")] + #[export] + chunk_size: Vector3i, + + #[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] + seed: i32, + + #[export(range = (0.01, 4.0, 0.01))] + frequency: f32, + + #[export(range = (1.0, 10.0))] + terrain_height: u32, +} + +#[godot_api] +impl IResource for WorldConfig { + fn init(base: Base) -> Self { + // DEFAULT Values + Self { + base, + chunk_size: Vector3i::new(32, 32, 32), + render_distance: 8, + worker_threads: 4, + seed: 1234, + frequency: 0.03, + terrain_height: 6, + } + } +} diff --git a/src/world.rs b/src/editor/world_node.rs similarity index 70% rename from src/world.rs rename to src/editor/world_node.rs index 131b633..7a82d79 100644 --- a/src/world.rs +++ b/src/editor/world_node.rs @@ -1,86 +1,45 @@ use godot::classes::{INode3D, VoxelGi}; use godot::prelude::*; -use crate::chunk::ChunkManager; use crate::editor::VoxelRegistry; +use crate::editor::world_config::WorldConfig; use crate::generation::SimpleSurfaceGenerator; use crate::generation::generator::SurfaceGenerator; -use crate::meshing::binary_greedy_mesher::BinaryGreedyMesher; use crate::meshing::{Mesher, TexturedMesher}; +use crate::terrain::TerrainManager; #[derive(GodotClass)] #[class(base=Node3D,tool)] pub struct World { base: Base, - // ===== RENDERING GROUP ===== - #[export_group(name = "Rendering")] + #[export_group(name = "Config")] + + /// World generation and rendering settings #[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, + config: Option>, #[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, + // runtime systems + terrain_manager: TerrainManager, + runtime_generator: Box, + runtime_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(); - + godot_print!("🧊 FastVoxel Init..."); 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, + config: None, registry: None, - voxelgi_node: None, + terrain_manager: TerrainManager::new(), + runtime_generator: Box::new(SimpleSurfaceGenerator::defaults()), + runtime_mesher: Box::new(TexturedMesher::new()), } } @@ -96,11 +55,6 @@ impl INode3D for World { 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, @@ -116,8 +70,32 @@ impl INode3D for World { } } +/*** + * 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 { + 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] fn generate_terrain(&mut self) { match &self.registry { 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/lib.rs b/src/lib.rs index 5f2e340..4a81006 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 terrain; struct FastVoxel; diff --git a/src/terrain/mod.rs b/src/terrain/mod.rs new file mode 100644 index 0000000..ddad7ed --- /dev/null +++ b/src/terrain/mod.rs @@ -0,0 +1,3 @@ +pub mod terrain_manager; + +pub use terrain_manager::TerrainManager; diff --git a/src/terrain/terrain_manager.rs b/src/terrain/terrain_manager.rs new file mode 100644 index 0000000..ea219c3 --- /dev/null +++ b/src/terrain/terrain_manager.rs @@ -0,0 +1,34 @@ +use std::collections::HashMap; + +use crate::{chunk::ChunkColumn, editor::world_config::WorldConfig, rendering::Renderer}; + +pub struct TerrainManager { + chunks: HashMap<(i32, i32), ChunkColumn>, + // renderer: Renderer, +} + +impl TerrainManager { + pub fn new(config: WorldConfig) -> Self { + // Terrain Size calculation + let width = config.get_render_distance() * 2 + 1; + let height = config.get_terrain_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::with_capacity(capacity), + } + } +} 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>, -}