Compare commits
8 Commits
old-impl
...
c1f240dd32
| Author | SHA1 | Date | |
|---|---|---|---|
| c1f240dd32 | |||
| ae7d82a542 | |||
| a1acd221a0 | |||
| d3c1b7bb5b | |||
| 166f9e4aa7 | |||
| c7b6ad917e | |||
| a1eb6dcce5 | |||
| d2d678de4a |
21
LICENSE
Normal file
21
LICENSE
Normal 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
132
README.md
Normal 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 divide‑and‑conquer 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
|
||||||
|
|
||||||
|
```
|
||||||
|

|
||||||
|
```
|
||||||
|
|
||||||
|
## 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 bit‑packed `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 per‑vertex 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.
|
||||||
20
build.sh
20
build.sh
@@ -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: Cross‑compile 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
220
docs/INTRODUCTION.MD
Normal 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
BIN
docs/screenshots/colors.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 248 KiB |
BIN
docs/screenshots/heightmap-noise.png
Normal file
BIN
docs/screenshots/heightmap-noise.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 MiB |
BIN
docs/screenshots/normal-map.png
Normal file
BIN
docs/screenshots/normal-map.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
44
src/editor/world_config.rs
Normal file
44
src/editor/world_config.rs
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
154
src/editor/world_generator.rs
Normal file
154
src/editor/world_generator.rs
Normal 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
153
src/editor/world_node.rs
Normal 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,
|
||||||
|
// ®istry_ref,
|
||||||
|
// self.surface_generator.as_ref(),
|
||||||
|
// self.mesher.as_ref(),
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn add_pending_mesh_instances_to_scene(&mut self) {
|
||||||
|
// let mesh_instances = self.chunk_manager.take_pending_mesh_instances();
|
||||||
|
// if mesh_instances.is_empty() {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// let mut base = self.base_mut();
|
||||||
|
|
||||||
|
// for mesh_instance in mesh_instances {
|
||||||
|
// base.add_child(&mesh_instance.upcast::<Node3D>());
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
@@ -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");
|
||||||
@@ -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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -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
5
src/runtime/mod.rs
Normal 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
42
src/runtime/runtime.rs
Normal 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
17
src/runtime/types.rs
Normal 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>,
|
||||||
|
}
|
||||||
197
src/world.rs
197
src/world.rs
@@ -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,
|
|
||||||
®istry_ref,
|
|
||||||
self.surface_generator.as_ref(),
|
|
||||||
self.mesher.as_ref(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_pending_mesh_instances_to_scene(&mut self) {
|
|
||||||
let mesh_instances = self.chunk_manager.take_pending_mesh_instances();
|
|
||||||
if mesh_instances.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let mut base = self.base_mut();
|
|
||||||
|
|
||||||
for mesh_instance in mesh_instances {
|
|
||||||
base.add_child(&mesh_instance.upcast::<Node3D>());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
pub struct WorldManager {
|
|
||||||
chunks: HashMap<(i32, i32), Chunk>,
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user