Compare commits

...

6 Commits

Author SHA1 Message Date
8a635640c5 Merge pull request 'Major Rewrite' (#1) from world_manager-impl into master
Reviewed-on: #1
2026-03-27 23:47:50 +00:00
d3c1b7bb5b re-working the editor nodes
de-coupling godot resources from internal runtime logic
2026-03-28 03:12:31 +03:30
166f9e4aa7 updated readme, added heightmap screenshot 2026-03-21 02:25:38 +03:30
c7b6ad917e updated readme + screenshots 2026-03-20 18:50:56 +03:30
a1eb6dcce5 updated readme 2026-03-20 18:42:51 +03:30
d2d678de4a re-working editor resource, chunk_manager and more. 2026-03-20 18:37:38 +03:30
29 changed files with 910 additions and 302 deletions

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.

384
README.md Normal file
View File

@@ -0,0 +1,384 @@
# FastVoxel
<p align="center">
<img src="./logo-cropped.svg" alt="FastVoxel logo" width="180" />
</p>
**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 divideandconquer parallelism.
## Screenshots
<p align="center">
<img src="docs/screenshots/colors.png" alt="Albedo Vertex color" width="960" />
</p>
<p align="center">
<img src="docs/screenshots/heightmap-noise.png" alt="Heightmap Surface Noise" width="960" />
</p>
<p align="center">
<img src="docs/screenshots/normal-map.png" alt="Albedo Texture + Normal map" width="960" />
</p>
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.

View File

@@ -17,7 +17,7 @@ TMP="$DEST_LINUX/lib${LIB_NAME}.so.tmp"
cp "target/release/lib${LIB_NAME}.so" "$TMP" cp "target/release/lib${LIB_NAME}.so" "$TMP"
mv "$TMP" "$DEST_LINUX/lib${LIB_NAME}.so" 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 # Uncomment the following lines if you have the Windows target installed
# echo "Building for Windows (cross-compile)..." # echo "Building for Windows (cross-compile)..."
# cargo build --release --target x86_64-pc-windows-gnu # cargo build --release --target x86_64-pc-windows-gnu

BIN
docs/screenshots/colors.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,7 @@
pub mod editor;
pub mod voxel_registry; 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; pub use voxel_registry::VoxelRegistry;

View File

@@ -7,7 +7,7 @@ use godot::{
prelude::{Export, GodotClass, GodotConvert, Var}, prelude::{Export, GodotClass, GodotConvert, Var},
}; };
#[derive(GodotConvert, Var, Export, Debug, Clone, Copy)] #[derive(GodotConvert, Var, Export, Debug, Clone, Copy, PartialEq)]
#[godot(via = i64)] #[godot(via = i64)]
pub enum VoxelType { pub enum VoxelType {
Empty, Empty,
@@ -58,26 +58,17 @@ pub struct VoxelModel {
impl VoxelModel { impl VoxelModel {
#[inline] #[inline]
pub fn is_solid(&self) -> bool { pub fn is_solid(&self) -> bool {
match self.voxel_type { self.voxel_type != VoxelType::Empty
VoxelType::Empty => false,
_ => true,
}
} }
#[inline] #[inline]
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
match self.voxel_type { self.voxel_type == VoxelType::Empty
VoxelType::Empty => true,
_ => false,
}
} }
#[inline] #[inline]
pub fn is_cube(&self) -> bool { pub fn is_cube(&self) -> bool {
match self.voxel_type { self.voxel_type == VoxelType::Cube
VoxelType::Cube => true,
_ => false,
}
} }
#[inline] #[inline]

View File

@@ -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<Resource>,
// ===== 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<Gd<WorldGeneratorResource>>,
#[export(range = (1.0, 10.0))]
surface_height: u8,
}
#[godot_api]
impl IResource for WorldConfig {
fn init(base: Base<Resource>) -> Self {
// DEFAULT Values
Self {
base,
render_distance: 8,
worker_threads: 8,
generator: None,
// noise: None,
surface_height: 6,
}
}
}

View File

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

153
src/editor/world_node.rs Normal file
View File

@@ -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<Node3D>,
#[export_group(name = "Config")]
/// World generation and rendering settings
#[export]
config: Option<Gd<WorldConfig>>,
#[export_group(name = "Voxel Registry")]
#[export]
registry: Option<Gd<VoxelRegistry>>,
state: WorldState,
}
pub enum WorldState {
Uninitialized,
Ready { runtime: Runtime },
}
#[godot_api]
impl INode3D for World {
fn init(base: Base<Node3D>) -> 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,
// &registry_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::<Node3D>());
// }
// }
}

View File

@@ -8,7 +8,7 @@ use godot::classes::Texture2D;
use godot::classes::editor_plugin::CustomControlContainer; use godot::classes::editor_plugin::CustomControlContainer;
use godot::prelude::*; use godot::prelude::*;
use crate::world::World; use crate::editor::world_node::World;
#[derive(GodotClass)] #[derive(GodotClass)]
#[class(init, base=EditorPlugin, tool)] #[class(init, base=EditorPlugin, tool)]
@@ -93,6 +93,11 @@ impl IEditorPlugin for WorldPlugin {
// --- Build the dropdown menu --- // --- Build the dropdown menu ---
let mut popup = menu_button.get_popup(); // Returns a PopupMenu 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("Rebuild World"); // item text, id
popup.as_mut().expect("no popup").add_item("Clear World"); popup.as_mut().expect("no popup").add_item("Clear World");
popup.as_mut().expect("no popup").add_item("Reload Chunks"); popup.as_mut().expect("no popup").add_item("Reload Chunks");

View File

@@ -1,15 +1,6 @@
use godot::classes::class_macros::private::virtuals::Os::Vector3i; use crate::chunk::Chunk;
use crate::chunk::{Chunk, ChunkColumn, column}; pub trait TerrainGenerator: Send + Sync {
fn sample_chunk(&self, column: &mut Chunk);
pub trait WorldGenerator: Send + Sync { fn sample_height(&self, x: f32, z: f32) -> f32;
fn generate_chunk(&self, chunk: &mut Chunk);
fn get_base_height(&self, x: f32, z: f32) -> f32;
fn should_be_solid(&self, density: f32, relative_height: f32) -> bool;
fn get_name(&self) -> &str;
}
pub trait SurfaceGenerator: Send + Sync {
fn generate(&self, column: &mut ChunkColumn);
fn sample_voxel(&self, pos: Vector3i) -> bool;
} }

View File

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

View File

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

View File

@@ -1,13 +1,13 @@
use godot::prelude::*; use godot::prelude::*;
mod voxel; mod voxel;
mod world;
mod chunk; mod chunk;
mod editor; mod editor;
mod generation; mod generation;
mod meshing; mod meshing;
mod rendering; mod rendering;
mod runtime;
struct FastVoxel; struct FastVoxel;

View File

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

View File

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

View File

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

View File

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

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

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

34
src/runtime/runtime.rs Normal file
View File

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

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

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

View File

@@ -1,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<Node3D>,
// ===== 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<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>,
}
#[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();
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,
&registry_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::<Node3D>());
}
}
}

View File

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