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** 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
+
+
+
+
+
+
+
+
+
+
+
+
+
+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
+
+```
+
+## 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>,
-}