Compare commits

8 Commits

Author SHA1 Message Date
c1f240dd32 windows build script, runtime impl 2026-04-15 10:54:58 +03:30
ae7d82a542 readme update 2026-04-15 10:54:08 +03:30
a1acd221a0 readme 2026-04-14 21:20:36 +03:30
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
30 changed files with 895 additions and 311 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.

132
README.md Normal file
View File

@@ -0,0 +1,132 @@
# FastVoxel
<p align="center">
<img src="./logo-cropped.svg" alt="FastVoxel logo" width="180" />
</p>
**FastVoxel** is a voxel engine for Godot written in Rust using GDExtension.
Main goals are:
- Generate chunks fast enough (It's already faster than minecraft, but not because of my good code, it's actually minecraft's fault)
- store voxels using almost no memory (because RAM prices blah blah blah.. and also I enjoy suffering so why not spend 1k hours optimizing this thing?),
- and build meshes at runtime for blocky worlds with textured materials.
This repo contains the engine side of the project.
Yes, things are constantly being refactored.
And yes, I still have no idea what I'm doing, so take that for granted and pray to the compiler.
## Highlights
- Rust-based Godot 4 GDExtension
- Chunked voxel mesh pipeline
- Bit-packed voxel storage (solid/air, liquid, whatever)
- Procedural terrain via `fastnoise-lite`
- Chunk streaming around the player
- Runtime cube meshing with texture atlas support (up to 1024x1024)
- Godot editor resources for config + voxel registry
## Soon
- Multi-threaded chunk meshing & generation using a **work stealing** thread pool with divideandconquer parallelism.
(Some C dev just segfaulted reading that.)
## Screenshots
<p align="center">
<img src="docs/screenshots/colors.png" width="960" />
</p>
<p align="center">
<img src="docs/screenshots/heightmap-noise.png" width="960" />
</p>
<p align="center">
<img src="docs/screenshots/normal-map.png" width="960" />
</p>
The repo mostly contains branding assets. More gameplay screenshots coming soon, assuming I stop rewriting the engine every Tuesday.
To add your own: put them in
```
docs/screenshots
```
then embed with
```
![image](docs/screenshots/my_face.png)
```
## What FastVoxel Actually Does
The engine generates a voxel world using chunk columns.
The surprisingly simple pipeline:
1. `SurfaceGenerator` decides if a voxel is solid.
2. `ChunkColumn` stacks chunks vertically.
3. Each `Chunk` stores voxels in bitpacked `u32`s.
4. `Mesher` converts only the exposed voxel faces into triangles.
5. `Renderer` throws those triangles into Godot's scene tree.
6. The world updates chunks as the player moves around.
Everything is modular because future "me", will absolutely regret today's design decisions and might decide to get back into the cave and rewrite the engine again.
## What FastVoxel _Doesn't_ Do (Yet)
1. No compute shaders or GPU meshing. This is an OS-Thread party.
2. No pervertex AO on the greedy mesher.
(Because it's complicated and my last functioning brain cell is currently writing this.)
3. Doesn't cull neighboring chunk faces yet because chunks don't talk to each other.
(They're socially anxious.)
## Core Concepts
### Chunked world layout
Terrain = columns of chunks.
Defaults:
- chunk size: `32 × 32 × 32`
- chunks load/unload based on render distance (default: 8)
Relevant files:
- `src/chunk/chunk.rs`
- `src/chunk/column.rs`
- `src/chunk/chunk_manager.rs`
### Bit-packed voxel storage
Instead of storing a big struct per voxel, we cram voxels into `u32`s.
Each voxel = **1 bit**:
- `0` => air
- `1` => solid
A single `u32` stores 32 voxels (very memory-friendly).
A 32×32×32 chunk:
- each row (32 voxels) = `u32`
- each layer = 32 rows = 32 `u32`s
- whole chunk = 32 × 32 = **1024 `u32`s**
This layout is amazing for meshing because checking solid/air is basically:
```
bit = (row >> x) & 1
```
Since we use 1 bit instead of a whole struct, one could argu the memory usage is ~32× lower.
Important, because RAM now costs $800 for 16 GB. that's like fifty bucks per gigabyte.
Materials are handled at the meshing/registry layer, not in voxel storage.
## License
MIT License. Do whatever, just don't blame me.

View File

@@ -4,25 +4,25 @@
set -e set -e
# Configuration # Configuration
GODOT_PROJECT="../minekoloft" # relative path to Godot project GODOT_PROJECT="../minekoloft"
LIB_NAME="fastvoxel" # your library name LIB_NAME="fastvoxel"
DEST_LINUX="$GODOT_PROJECT/addons/fastvoxel/bin/linux" DEST_LINUX="$GODOT_PROJECT/addons/fastvoxel/bin/linux"
DEST_WINDOWS="$GODOT_PROJECT/addons/fastvoxel/bin/windows" DEST_WINDOWS="$GODOT_PROJECT/addons/fastvoxel/bin/windows"
# Build for Linux (native) # Build for Linux (native)
echo "Building for Linux (native)..." echo "Building for Linux (native)..."
cargo build --release cargo build --release --target x86_64-unknown-linux-gnu
# Copy the shared library # Copy the shared library
TMP="$DEST_LINUX/lib${LIB_NAME}.so.tmp" TMP="$DEST_LINUX/lib${LIB_NAME}.so.tmp"
cp "target/release/lib${LIB_NAME}.so" "$TMP" cp "target/x86_64-unknown-linux-gnu/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
# mkdir -p "$DEST_WINDOWS" mkdir -p "$DEST_WINDOWS"
# cp "target/x86_64-pc-windows-gnu/release/${LIB_NAME}.dll" "$DEST_WINDOWS/" cp "target/x86_64-pc-windows-gnu/release/${LIB_NAME}.dll" "$DEST_WINDOWS/"
# echo "Copied to $DEST_WINDOWS" echo "Copied to $DEST_WINDOWS"
echo "Done." echo "Done."

220
docs/INTRODUCTION.MD Normal file
View File

@@ -0,0 +1,220 @@
# 📄 Introduction into FastVoxel
## Surface generation
Default generator: `SimpleSurfaceGenerator`
It uses `fastnoise-lite` to create a heightmap, then fills voxel columns from the bottom up.
Files:
- `src/generation/generator.rs`
- `src/generation/simple_heightmap.rs`
Generation uses traits, so you can plug in whatever mumbo jumbo nonsense you want later: caves, biomes, multiverse, whatever.
## Meshing
Multiple meshers exist because one was simply not enough to satisfy my unhealthy obsession with "doing it right this time."
[engine development perfectionism disorder. there's no cure.]
- `CullingMesher` basic visible-face meshing
- `TexturedMesher` UV-aware cube meshing (Minecraft basically)
- `BinaryGreedyMesher` experimental optimization attempt
Files:
- `src/meshing/mesher.rs`
- `src/meshing/textured_mesher.rs`
- `src/meshing/binary_greedy_mesher.rs`
The textured mesher is currently the most practical one because it respects UVs and etc. simply because you can define the material for it.
It's not optimized though.
## Godot integration
The engine exposes several handy APIs for the editor:
- `WorldConfig` generation settings
- `VoxelRegistry` voxel definitions and atlas stuff
- `World` main terrain node
- `WorldPlugin` editor 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, manager
├── editor/ # Godot resources + editor plugin
├── generation/ # terrain generators
├── meshing/ # meshing algorithems
├── rendering/ # mesh to Godot conversion
├── terrain/ # I'm not so sure
└── voxel/ # registry + older voxel logic
```
## Build and Install
Requirements:
- Rust toolchain
- Godot 4 project with GDExtension
- Optional sibling project for `build.sh` to work.
### Build the extension
```
cargo build --release
```
### Helper script
`build.sh` builds & copies the extension to:
```
../minekoloft/addons/fastvoxel/bin/linux
```
Run with:
```
./build.sh
```
If your project is elsewhere, edit the path.
## Using It in Godot
Typical use:
1. build the Rust extension
2. install addon in your Godot project
3. create `WorldConfig`
4. create `VoxelRegistry`
5. add `World` node
6. assign config + registry
7. generate terrain
8. Call the "update with player position" API every frame. Spam it. It returns early and won't yell at you.
### WorldConfig
Contains:
- chunk_size
- render_distance
- worker_threads
- seed
- frequency
- terrain_height
Defaults:
- chunk: `32,32,32`
- render dist: `8`
- threads: `4`
- seed: `1234`
- freq: `0.03`
- terrain height: `6`
### VoxelRegistry
Each voxel defines:
- type
- atlas size
- tile index per face (top, bottom, left, right, front, back)
Mesher uses this for UVs.
### Runtime API
World node provides:
- `generate_terrain()`
- `regenerate_terrain()`
- `clear_terrain()`
- `update_from_gdscript(player_world_pos)`
GDScript example:
```gdscript
@onready var world = $World
@onready var player = $Player
func _ready():
world.generate_terrain()
func _process(_delta):
world.update_from_gdscript(player.global_position)
```
## Engine-Level API Notes
### Generation trait
```rust
pub trait SurfaceGenerator: Send + Sync {
fn generate(&self, column: &mut ChunkColumn);
fn sample_voxel(&self, pos: Vector3i) -> bool;
}
```
### Mesher trait
```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;
}
```
### ChunkManager
Handles:
- chunk lifetime
- generation around player
- unloading distant chunks
- meshing
- sending meshes to renderer
- attaching instances to scene
Basically the project's overworked intern.
## Current State
Mid-development, active refactors, might panic on unwrap.
Direction is clear though:
- compact chunk storage
- trait-based generation
- modular meshing
- Godot resource workflow
- runtime streaming
## Why This Document Exists
Because why not? If you actually wanted to read the code, you wouldn't be here.
## Future Work
- finish terrain/world refactor
- more screenshots
- stable API for godot
- multithreaded chunk gen (**GOD HELP**)
- Better Godot material integration for voxel meshes (so things look less like programmer art).
- fix the greedy meshing
- sample Godot project

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::*;

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

@@ -0,0 +1,42 @@
use std::collections::HashMap;
use crate::{chunk::Chunk, runtime::types::WorldConfig};
pub enum RuntimeState {
Idle,
ReadingStorage,
Generating,
Meshing,
SavingToStorage,
}
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>,
}