Major Rewrite #1

Merged
TheRealKaamy merged 5 commits from world_manager-impl into master 2026-03-27 23:47:51 +00:00
12 changed files with 523 additions and 81 deletions
Showing only changes of commit d2d678de4a - Show all commits

21
LICENSE Normal file
View File

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

362
README.md Normal file
View File

@@ -0,0 +1,362 @@
# FastVoxel
<p align="center">
<img src="./logo-cropped.svg" alt="FastVoxel logo" width="180" />
</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.
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.
<p align="center">
<img src="./logo-cropped.svg" alt="FastVoxel mark" width="220" />
</p>
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.

View File

@@ -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: Crosscompile 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

View File

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

View File

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

View File

@@ -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<Resource>,
// ===== 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<Resource>) -> 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,
}
}
}

View File

@@ -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<Node3D>,
// ===== 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<Gd<WorldConfig>>,
#[export_group(name = "Voxel Registry")]
#[export]
registry: Option<Gd<VoxelRegistry>>,
#[export_group(name = "GI")]
#[export]
voxelgi_node: Option<Gd<VoxelGi>>,
// World data (temporary)
chunk_manager: ChunkManager,
surface_generator: Box<dyn SurfaceGenerator>,
mesher: Box<dyn Mesher>,
// runtime systems
terrain_manager: TerrainManager,
runtime_generator: Box<dyn SurfaceGenerator>,
runtime_mesher: Box<dyn Mesher>,
}
#[godot_api]
impl INode3D for World {
fn init(base: Base<Node3D>) -> 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 {

View File

@@ -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");

View File

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

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

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

View File

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

View File

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