From a47f1f98187c4ca0f93b9d6d033b0a8ce0920f0f Mon Sep 17 00:00:00 2001 From: TheRealKaamy Date: Thu, 12 Mar 2026 19:25:03 +0330 Subject: [PATCH] messy state --- .cargo/config.toml | 2 + .gitignore | 2 + Cargo.lock | 228 ++++++++++++++++ Cargo.toml | 21 ++ build.sh | 28 ++ logo-cropped.svg | 124 +++++++++ logo.svg | 125 +++++++++ src/chunk/chunk.rs | 108 ++++++++ src/chunk/chunk_manager.rs | 229 ++++++++++++++++ src/chunk/column.rs | 89 ++++++ src/chunk/mesh.rs | 40 +++ src/chunk/mod.rs | 9 + src/editor/mod.rs | 3 + src/editor/voxel_registry.rs | 95 +++++++ src/generation/generator.rs | 8 + src/generation/heightmap.rs | 202 ++++++++++++++ src/generation/layered_generator.rs | 39 +++ src/generation/mod.rs | 7 + src/lib.rs | 15 + src/meshing/binary_greedy_mesher.rs | 317 +++++++++++++++++++++ src/meshing/mesher.rs | 207 ++++++++++++++ src/meshing/mod.rs | 7 + src/meshing/textured_mesher.rs | 410 ++++++++++++++++++++++++++++ src/rendering/mod.rs | 3 + src/rendering/renderer.rs | 220 +++++++++++++++ src/voxel/mod.rs | 7 + src/voxel/model.rs | 24 ++ src/voxel/registry.rs | 13 + src/voxel/voxel.rs | 33 +++ src/world.rs | 176 ++++++++++++ 30 files changed, 2791 insertions(+) create mode 100644 .cargo/config.toml create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100755 build.sh create mode 100644 logo-cropped.svg create mode 100644 logo.svg create mode 100644 src/chunk/chunk.rs create mode 100644 src/chunk/chunk_manager.rs create mode 100644 src/chunk/column.rs create mode 100644 src/chunk/mesh.rs create mode 100644 src/chunk/mod.rs create mode 100644 src/editor/mod.rs create mode 100644 src/editor/voxel_registry.rs create mode 100644 src/generation/generator.rs create mode 100644 src/generation/heightmap.rs create mode 100644 src/generation/layered_generator.rs create mode 100644 src/generation/mod.rs create mode 100644 src/lib.rs create mode 100644 src/meshing/binary_greedy_mesher.rs create mode 100644 src/meshing/mesher.rs create mode 100644 src/meshing/mod.rs create mode 100644 src/meshing/textured_mesher.rs create mode 100644 src/rendering/mod.rs create mode 100644 src/rendering/renderer.rs create mode 100644 src/voxel/mod.rs create mode 100644 src/voxel/model.rs create mode 100644 src/voxel/registry.rs create mode 100644 src/voxel/voxel.rs create mode 100644 src/world.rs diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..03f4879 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +rustflags = ["-Ctarget-cpu=native"] diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3a8cabc --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +.idea diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..3a4f516 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,228 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "fastnoise-lite" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e5f3c3cc9081e5d0e18bcd50e80cd33cba47fc22f88a9da9c33ecd1c87ea5c0" +dependencies = [ + "num-traits", +] + +[[package]] +name = "fastvoxel" +version = "0.1.0" +dependencies = [ + "fastnoise-lite", + "godot", +] + +[[package]] +name = "gdextension-api" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e25d88dabe9fdb2e064cb545312178ec4025eefb62d9a3bbce1769088e2c381d" + +[[package]] +name = "glam" +version = "0.30.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd47b05dddf0005d850e5644cae7f2b14ac3df487979dbfff3b56f20b1a6ae46" + +[[package]] +name = "godot" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a9b5fac6f08345fe81f840353b433c3e8cd07a105cb23ea080251173c149625" +dependencies = [ + "godot-core", + "godot-macros", +] + +[[package]] +name = "godot-bindings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96920225203f1ccee3d1fd4513402a31a4d6567e437367c704682f7eee43eda3" +dependencies = [ + "gdextension-api", +] + +[[package]] +name = "godot-cell" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb34522bce7b863ef4dbf154be53392f23e32747877b4c7e759a0ec43876b3f" + +[[package]] +name = "godot-codegen" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396303aaca6c212cf85442e78afcf28844b06b564bbc23f37c40ec57885f4395" +dependencies = [ + "godot-bindings", + "heck", + "nanoserde", + "proc-macro2", + "quote", + "regex", +] + +[[package]] +name = "godot-core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be623c121c8cd24a0e173771d73a0cbf8180840abd4389f4c78e698018d729ae" +dependencies = [ + "glam", + "godot-bindings", + "godot-cell", + "godot-codegen", + "godot-ffi", +] + +[[package]] +name = "godot-ffi" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a8358c83761a174b33640944e72ec2fabf0274f516520b68ab8a140ac25d97" +dependencies = [ + "godot-bindings", + "godot-codegen", + "godot-macros", + "libc", +] + +[[package]] +name = "godot-macros" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96066983ab510b0f89ea2f5b3e1dc47eaf9126f0f9e595da6cba4e7bf49a84dd" +dependencies = [ + "godot-bindings", + "proc-macro2", + "quote", + "venial", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "libc" +version = "0.2.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "nanoserde" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a36fb3a748a4c9736ed7aeb5f2dfc99665247f1ce306abbddb2bf0ba2ac530a4" +dependencies = [ + "nanoserde-derive", +] + +[[package]] +name = "nanoserde-derive" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a846cbc04412cf509efcd8f3694b114fc700a035fb5a37f21517f9fb019f1ebc" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "venial" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a42528baceab6c7784446df2a10f4185078c39bf73dc614f154353f1a6b1229" +dependencies = [ + "proc-macro2", + "quote", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..37692c4 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "fastvoxel" +version = "0.1.0" +edition = "2024" + +[dependencies] +godot = "0.4.2" +fastnoise-lite = "*" + +[lib] +crate-type = ["cdylib"] + +[profile.all] +inherits = "release" +opt-level = 3 +debug = false +strip = true +lto = true +codegen-units = 1 +incremental = false +panic = "abort" diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..e12a397 --- /dev/null +++ b/build.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +# Exit on error +set -e + +# Configuration +GODOT_PROJECT="../minekoloft" # relative path to Godot project +LIB_NAME="fastvoxel" # your library name +DEST_LINUX="$GODOT_PROJECT/addons/fastvoxel/bin/linux" +DEST_WINDOWS="$GODOT_PROJECT/addons/fastvoxel/bin/windows" + +# Build for Linux (native) +echo "Building for Linux (native)..." +cargo build --release +# Copy the shared library +mkdir -p "$DEST_LINUX" +cp "target/release/lib${LIB_NAME}.so" "$DEST_LINUX/" +echo "Copied to $DEST_LINUX" + +# Optional: Cross‑compile for Windows (if needed) +# Uncomment the following lines if you have the Windows target installed +echo "Building for Windows (cross-compile)..." +cargo build --release --target x86_64-pc-windows-gnu +mkdir -p "$DEST_WINDOWS" +cp "target/x86_64-pc-windows-gnu/release/${LIB_NAME}.dll" "$DEST_WINDOWS/" +echo "Copied to $DEST_WINDOWS" + +echo "Done." \ No newline at end of file diff --git a/logo-cropped.svg b/logo-cropped.svg new file mode 100644 index 0000000..7c7baf7 --- /dev/null +++ b/logo-cropped.svg @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/logo.svg b/logo.svg new file mode 100644 index 0000000..565e9e6 --- /dev/null +++ b/logo.svg @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/chunk/chunk.rs b/src/chunk/chunk.rs new file mode 100644 index 0000000..4a5626f --- /dev/null +++ b/src/chunk/chunk.rs @@ -0,0 +1,108 @@ +use godot::prelude::*; + +pub const CHUNK_SIZE: i32 = 32; +pub const ARR_SIZE: usize = CHUNK_SIZE.pow(3) as usize; +// 32x32x32 = 32768 bits, stored in u32s (32 bits each) = 1024 u32s +pub const BITPACKED_SIZE: usize = ARR_SIZE / 32; + +#[derive(Debug)] +pub struct Chunk { + // Bitpacked voxel data: 0 = air, 1 = solid + pub voxels: Vec, + // WORLD COORDINATES + pub world_position: (f64, f64, f64), + modified: bool, +} + +impl Chunk { + /// 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, size: Vector3i) -> Self { + Chunk { + voxels: vec![0u32; BITPACKED_SIZE], + world_position: (world_pos_x, world_pos_y, world_pos_z), + modified: false, + } + } + + #[inline] + pub fn get_voxel_index(&self, local_pos: Vector3i, chunk_size: Vector3i) -> usize { + ((local_pos.x * chunk_size.y * chunk_size.z) + (local_pos.y * chunk_size.x) + local_pos.z) + as usize + } + + #[inline] + pub fn is_voxel_within_bounds(&self, local_pos: Vector3i, chunk_size: Vector3i) -> bool { + local_pos.x >= 0 + && local_pos.x < chunk_size.x + && local_pos.y >= 0 + && local_pos.y < chunk_size.y + && local_pos.z >= 0 + && local_pos.z < chunk_size.z + } + + #[inline] + pub fn get_voxel(&self, local_pos: Vector3i, chunk_size: Vector3i) -> bool { + debug_assert!( + self.is_voxel_within_bounds(local_pos, chunk_size), + "Voxel position {:?} out of chunk bounds", + local_pos + ); + + let index = self.get_voxel_index(local_pos, chunk_size); + debug_assert!( + index < ARR_SIZE, + "Voxel index {:?} out of chunk bounds", + index + ); + + // Get the u32 containing this bit and the bit position within it + let word_index = index / chunk_size.x as usize; + let bit_index = index % chunk_size.x as usize; + + (self.voxels[word_index] & (1 << bit_index)) != 0 + } + + pub fn set_voxel(&mut self, voxel_pos: Vector3i, is_solid: bool, chunk_size: Vector3i) { + debug_assert!( + self.is_voxel_within_bounds(voxel_pos, chunk_size), + "Voxel position {:?} out of chunk bounds", + voxel_pos + ); + let index = self.get_voxel_index(voxel_pos, chunk_size); + + let word_index = index / chunk_size.x as usize; + let bit_index = index % chunk_size.x as usize; + + if is_solid { + self.voxels[word_index] |= 1 << bit_index; + } else { + self.voxels[word_index] &= !(1 << bit_index); + } + self.modified = true; + } + + pub fn fill(&mut self, is_solid: bool) { + let fill_value = if is_solid { u32::MAX } else { 0 }; + self.voxels.fill(fill_value); + self.modified = true; + } + + pub fn get_position(&self) -> (f64, f64, f64) { + self.world_position + } + + pub fn get_world_bounds(&self, chunk_size: Vector3i) -> (Vector3, Vector3) { + let (x, y, z) = self.world_position; + let min = Vector3::new( + (x * chunk_size.x as f64) as f32, + (y * chunk_size.y as f64) as f32, + (z * chunk_size.z as f64) as f32, + ); + let max = Vector3::new( + ((x + 1.0) * chunk_size.x as f64) as f32, + ((y + 1.0) * chunk_size.y as f64) as f32, + ((z + 1.0) * chunk_size.z as f64) as f32, + ); + (min, max) + } +} diff --git a/src/chunk/chunk_manager.rs b/src/chunk/chunk_manager.rs new file mode 100644 index 0000000..963fffc --- /dev/null +++ b/src/chunk/chunk_manager.rs @@ -0,0 +1,229 @@ +use godot::classes::MeshInstance3D; +use godot::classes::class_macros::private::virtuals::Os::{Vector3, Vector3i}; +use godot::global::godot_print; +use godot::obj::Gd; + +use crate::chunk::{Chunk, ChunkColumn, ChunkMesh}; +use crate::editor::voxel_registry::VoxelRegistry; +use crate::generation::WorldGenerator; +use crate::meshing::Mesher; +use crate::rendering::Renderer; +use std::collections::HashMap; +use std::time::Instant; + +pub struct ChunkManager { + pub chunk_columns: HashMap<(i32, i32), ChunkColumn>, + pub renderer: Renderer, + pub last_player_chunk: (i32, i32), + + // terrain parameteres + pub chunk_size: Vector3i, + pub terrain_height: u32, + + // Track which mesh instances need to be added to scene inefficient as but who cares + // the mesher is efficient enough + pub pending_mesh_instances: Vec>, +} + +impl ChunkManager { + pub fn new() -> Self { + Self { + chunk_columns: HashMap::new(), + renderer: Renderer::new(), + last_player_chunk: (0, 0), + chunk_size: Vector3i::ZERO, + terrain_height: 0, + pending_mesh_instances: Vec::new(), + } + } + + pub fn set_chunk_size(&mut self, chunk_size: Vector3i) { + self.chunk_size = chunk_size + } + + pub fn update_around_player( + &mut self, + player_world_pos: Vector3, + render_distance: i32, + registry: &VoxelRegistry, + generator: &dyn WorldGenerator, + mesher: &dyn Mesher, + ) -> bool { + let player_chunk_x = player_world_pos.x as i32 / self.chunk_size.x; + let player_chunk_z = player_world_pos.z as i32 / self.chunk_size.z; + let current_chunk = (player_chunk_x, player_chunk_z); + + // Early return if player is still in the same chunk + if current_chunk == self.last_player_chunk { + return false; + } + + let now = Instant::now(); + + godot_print!("Player moved to chunk: {:?}", current_chunk); + self.last_player_chunk = current_chunk; + + // Clear pending instances from previous updates + self.pending_mesh_instances.clear(); + + // Unload distant chunks + self.unload_distant_chunks(player_chunk_x, player_chunk_z, render_distance); + + let after_unload = now.elapsed().as_micros(); + + // Load new chunks + self.load_chunks_around( + player_chunk_x, + player_chunk_z, + render_distance, + registry, + generator, + mesher, + ); + + godot_print!( + "Loading {}ms || Unloading {}us, ", + (now.elapsed().as_micros() - after_unload) / 1000, + after_unload + ); + + true + } + + fn unload_distant_chunks(&mut self, center_x: i32, center_z: i32, distance: i32) { + let mut to_remove = Vec::new(); + + for (&(x, z), _) in &self.chunk_columns { + let dx = (x - center_x).abs(); + let dz = (z - center_z).abs(); + + if dx > distance || dz > distance { + to_remove.push((x, z)); + } + } + + for (x, z) in to_remove { + if let Some(_) = self.chunk_columns.remove(&(x, z)) { + // Remove all chunk meshes in this column + for y in 0..8 { + self.renderer.remove_chunk((x, y, z)); + } + godot_print!("Unloaded chunk column ({}, {})", x, z); + } + } + } + + fn load_chunks_around( + &mut self, + center_x: i32, + center_z: i32, + distance: i32, + registry: &VoxelRegistry, + generator: &dyn WorldGenerator, + mesher: &dyn Mesher, + ) { + let mut chunks_loaded = 0; + + for x in (center_x - distance)..=(center_x + distance) { + for z in (center_z - distance)..=(center_z + distance) { + if !self.chunk_columns.contains_key(&(x, z)) { + self.generate_chunk_column(x, z, registry, generator, mesher); + chunks_loaded += 1; + godot_print!("Loaded chunk column ({}, {})", x, z) + } + } + } + + if chunks_loaded > 0 { + godot_print!("Loaded {} new chunk columns", chunks_loaded); + } + } + + pub fn generate_chunk_column( + &mut self, + x: i32, + z: i32, + registry: &VoxelRegistry, + generator: &dyn WorldGenerator, + mesher: &dyn Mesher, + ) { + let mut column = ChunkColumn::new(x, z, self.chunk_size); + + let start = Instant::now(); + + // Generate all chunks first + for i in 0..self.terrain_height { + let chunk = column.get_or_create_chunk(i as i32); + generator.generate_chunk(chunk); + } + + let generate_elapsed = start.elapsed().as_micros(); + godot_print!("Generation took: {}μs", generate_elapsed); + + // Mesh and render each chunk + for i in 0..self.terrain_height { + if let Some(chunk) = column.get_chunk(i as usize) { + self.mesh_and_render_chunk(chunk, mesher, registry); + } + } + + // Insert the column only after we're completely done with it + self.chunk_columns.insert((x, z), column); + } + + fn mesh_and_render_chunk( + &mut self, + chunk: &Chunk, + mesher: &dyn Mesher, + registry: &VoxelRegistry, + ) { + let mut mesh = ChunkMesh::new(); + + let start = Instant::now(); + mesher.generate_mesh_with_registry(chunk, registry, &mut mesh); + + if mesh.is_empty() { + // Silent - empty chunks are normal + return; + } + + godot_print!("Meshing Took: {}μs", start.elapsed().as_micros()); + + let start = Instant::now(); + // Use render_chunk (with colors) instead of render_chunk_with_uvs + let mesh_instance = self.renderer.render_chunk(chunk, &mesh); + + godot_print!("Rendering Took: {}μs", start.elapsed().as_micros()); + self.pending_mesh_instances.push(mesh_instance); + } + + pub fn clear(&mut self) { + // Remove all mesh instances from renderer + self.renderer.clear(); + self.chunk_columns.clear(); + self.pending_mesh_instances.clear(); + // self.last_player_chunk = (0, 0); + } + + pub fn get_voxel_at(&self, world_pos: Vector3) -> bool { + // Convert world position to chunk coordinates + let chunk_x = (world_pos.x / 32.0).floor() as i32; + let chunk_z = (world_pos.z / 32.0).floor() as i32; + + if let Some(column) = self.chunk_columns.get(&(chunk_x, chunk_z)) { + // Pass the world position directly - let column handle the conversion + column.get_voxel(Vector3i::new( + world_pos.x as i32, // Use actual world coordinates + world_pos.y as i32, + world_pos.z as i32, + )) + } else { + false // Air by default + } + } + + // Get pending mesh instances and clear the list + pub fn take_pending_mesh_instances(&mut self) -> Vec> { + std::mem::take(&mut self.pending_mesh_instances) + } +} diff --git a/src/chunk/column.rs b/src/chunk/column.rs new file mode 100644 index 0000000..1536cfa --- /dev/null +++ b/src/chunk/column.rs @@ -0,0 +1,89 @@ +use godot::classes::class_macros::private::virtuals::Os::Vector3i; + +use super::chunk::{CHUNK_SIZE, Chunk}; + +pub const CHUNKS_PER_COLUMN: usize = 8; + +pub struct ChunkColumn { + // from bottom to top + // TODO: replace with vec![] + pub chunks: [Option; CHUNKS_PER_COLUMN], + pub world_position: (i32, i32), // XZ + pub chunk_size: Vector3i, +} + +impl ChunkColumn { + pub fn new(x: i32, z: i32, chunk_size: Vector3i) -> Self { + ChunkColumn { + chunks: [None, None, None, None, None, None, None, None], + world_position: (x, z), + chunk_size, + } + } + + #[inline] + pub fn get_chunk_index(chunk_size_y: i32, world_y: i32) -> usize { + (world_y / chunk_size_y) as usize + } + + #[inline] + pub fn get_local_y(chunk_size_y: i32, world_y: i32) -> i32 { + world_y % chunk_size_y + } + + #[inline] + pub fn get_world_y(chunk_size_y: i32, chunk_index: i32, local_y: i32) -> i32 { + (chunk_index * chunk_size_y) + local_y + } + + pub fn get_or_create_chunk(&mut self, chunk_y_index: i32) -> &mut Chunk { + if self.chunks[chunk_y_index as usize].is_none() { + let (world_x, world_z) = self.world_position; + self.chunks[chunk_y_index as usize] = Some(Chunk::new( + world_x as f64, + chunk_y_index as f64, + world_z as f64, + self.chunk_size, + )); + } + self.chunks[chunk_y_index as usize].as_mut().unwrap() + } + + pub fn get_chunk(&self, chunk_y_index: usize) -> Option<&Chunk> { + self.chunks[chunk_y_index].as_ref() + } + + pub fn get_chunk_mut(&mut self, chunk_y_index: usize) -> Option<&mut Chunk> { + self.chunks[chunk_y_index].as_mut() + } + + pub fn set_voxel(&mut self, world_pos: Vector3i, is_solid: bool) -> bool { + let chunk_index = Self::get_chunk_index(self.chunk_size.y, world_pos.y); + if chunk_index < CHUNKS_PER_COLUMN { + let size = self.chunk_size.clone(); + let chunk = self.get_or_create_chunk(chunk_index as i32); + + let local_x = world_pos.x.rem_euclid(size.x); + let local_y = Self::get_local_y(size.y, world_pos.y); + let local_z = world_pos.z.rem_euclid(size.z); + + chunk.set_voxel(Vector3i::new(local_x, local_y, local_z), is_solid, size); + true + } else { + false + } + } + + pub fn get_voxel(&self, world_pos: Vector3i) -> bool { + let chunk_index = Self::get_chunk_index(self.chunk_size.y, world_pos.y); + if let Some(chunk) = self.get_chunk(chunk_index) { + let local_x = world_pos.x.rem_euclid(CHUNK_SIZE); + let local_y = Self::get_local_y(self.chunk_size.y, world_pos.y); + let local_z = world_pos.z.rem_euclid(CHUNK_SIZE); + + chunk.get_voxel(Vector3i::new(local_x, local_y, local_z), self.chunk_size) + } else { + false // Air by default + } + } +} diff --git a/src/chunk/mesh.rs b/src/chunk/mesh.rs new file mode 100644 index 0000000..2f72904 --- /dev/null +++ b/src/chunk/mesh.rs @@ -0,0 +1,40 @@ +use godot::prelude::*; + +#[derive(Debug)] +pub struct ChunkMesh { + pub vertices: Vec, + pub normals: Vec, + pub colors: Vec, + pub uvs: Vec, + pub indices: Vec, +} + +impl Default for ChunkMesh { + fn default() -> Self { + Self::new() + } +} + +impl ChunkMesh { + pub fn new() -> Self { + Self { + vertices: Vec::new(), + normals: Vec::new(), + colors: Vec::new(), + uvs: Vec::new(), + indices: Vec::new(), + } + } + + pub fn clear(&mut self) { + self.vertices.clear(); + self.normals.clear(); + self.colors.clear(); + self.uvs.clear(); + self.indices.clear(); + } + + pub fn is_empty(&self) -> bool { + self.vertices.is_empty() + } +} diff --git a/src/chunk/mod.rs b/src/chunk/mod.rs new file mode 100644 index 0000000..e814609 --- /dev/null +++ b/src/chunk/mod.rs @@ -0,0 +1,9 @@ +pub mod chunk; +pub mod chunk_manager; +pub mod column; +pub mod mesh; + +pub use chunk::Chunk; +pub use chunk_manager::ChunkManager; +pub use column::ChunkColumn; +pub use mesh::ChunkMesh; diff --git a/src/editor/mod.rs b/src/editor/mod.rs new file mode 100644 index 0000000..f136d11 --- /dev/null +++ b/src/editor/mod.rs @@ -0,0 +1,3 @@ +pub mod voxel_registry; + +pub use voxel_registry::VoxelRegistry; diff --git a/src/editor/voxel_registry.rs b/src/editor/voxel_registry.rs new file mode 100644 index 0000000..1a798ce --- /dev/null +++ b/src/editor/voxel_registry.rs @@ -0,0 +1,95 @@ +use godot::{ + classes::{ + Resource, + class_macros::private::virtuals::Os::{Array, Vector2i}, + }, + obj::{Base, Gd}, + prelude::{Export, GodotClass, GodotConvert, Var}, +}; + +#[derive(GodotConvert, Var, Export, Debug, Clone, Copy)] +#[godot(via = i64)] +pub enum VoxelType { + Empty, + Cube, + Mesh, + Fluid, +} + +impl Default for VoxelType { + fn default() -> Self { + Self::Empty + } +} + +#[derive(GodotClass, Debug)] +#[class(init, base=Resource, tool)] +pub struct VoxelRegistry { + #[export] + pub models: Array>, + + base: Base, +} + +#[derive(GodotClass, Debug, Clone, Copy)] +#[class(init, base=Resource, tool)] +pub struct VoxelModel { + #[export] + voxel_type: VoxelType, + + #[export] + atlas_size_in_tiles: Vector2i, + + #[export_group(name = "Cube Tiles")] + #[export] + tile_left: Vector2i, + #[export] + tile_right: Vector2i, + #[export] + tile_bottom: Vector2i, + #[export] + tile_top: Vector2i, + #[export] + tile_back: Vector2i, + #[export] + tile_front: Vector2i, +} + +impl VoxelModel { + #[inline] + pub fn is_solid(&self) -> bool { + match self.voxel_type { + VoxelType::Empty => false, + _ => true, + } + } + + #[inline] + pub fn is_empty(&self) -> bool { + match self.voxel_type { + VoxelType::Empty => true, + _ => false, + } + } + + #[inline] + pub fn is_cube(&self) -> bool { + match self.voxel_type { + VoxelType::Cube => true, + _ => false, + } + } + + #[inline] + pub fn get_tile_for_face(&self, face: i32) -> Vector2i { + match face { + 0 => self.tile_left, // Left + 1 => self.tile_right, // Right + 2 => self.tile_bottom, // Bottom + 3 => self.tile_top, // Top + 4 => self.tile_back, // Back + 5 => self.tile_front, // Front + _ => Vector2i::new(0, 0), + } + } +} diff --git a/src/generation/generator.rs b/src/generation/generator.rs new file mode 100644 index 0000000..c7aee1e --- /dev/null +++ b/src/generation/generator.rs @@ -0,0 +1,8 @@ +use crate::chunk::Chunk; + +pub trait WorldGenerator: Send + Sync { + fn generate_chunk(&self, chunk: &mut Chunk); + fn get_base_height(&self, x: f32, z: f32) -> f32; + fn should_be_solid(&self, density: f32, relative_height: f32) -> bool; + fn get_name(&self) -> &str; +} diff --git a/src/generation/heightmap.rs b/src/generation/heightmap.rs new file mode 100644 index 0000000..ccd5078 --- /dev/null +++ b/src/generation/heightmap.rs @@ -0,0 +1,202 @@ +use super::generator::WorldGenerator; +use crate::chunk::Chunk; +use crate::chunk::chunk::CHUNK_SIZE; +use crate::voxel::Voxel; +use fastnoise_lite::*; +use godot::classes::class_macros::private::virtuals::Os::{Vector2i, Vector3i}; + +pub struct HeightmapGenerator { + noise: FastNoiseLite, + noise_detail: FastNoiseLite, + noise_caves: FastNoiseLite, + terrain_height: i32, + chunk_size: Vector3i, +} + +impl HeightmapGenerator { + pub fn new(seed: i64, frequency: f32, terrain_height: i32) -> Self { + // Main terrain noise + let mut noise = FastNoiseLite::new(); + noise.set_seed(Some(seed as i32)); + noise.set_frequency(Some(frequency * 0.5)); // Lower frequency for larger features + noise.set_noise_type(Some(fastnoise_lite::NoiseType::OpenSimplex2S)); + noise.set_fractal_type(Some(fastnoise_lite::FractalType::FBm)); + noise.set_fractal_octaves(Some(4)); + noise.set_fractal_lacunarity(Some(2.0)); + noise.set_fractal_gain(Some(0.5)); + + // Detail noise for small features + let mut noise_detail = FastNoiseLite::new(); + noise_detail.set_seed(Some(seed as i32 + 1)); + noise_detail.set_frequency(Some(frequency * 2.0)); + noise_detail.set_noise_type(Some(fastnoise_lite::NoiseType::OpenSimplex2S)); + + // Cave noise + let mut noise_caves = FastNoiseLite::new(); + noise_caves.set_seed(Some(seed as i32 + 2)); + noise_caves.set_frequency(Some(frequency * 1.5)); + noise_caves.set_noise_type(Some(fastnoise_lite::NoiseType::Perlin)); + + Self { + noise, + noise_detail, + noise_caves, + terrain_height, + chunk_size: Vector3i::new(32, 32, 32), + } + } +} + +impl WorldGenerator for HeightmapGenerator { + // fn generate_chunk(&self, chunk: &mut Chunk) { + // let (chunk_x, chunk_y, chunk_z) = chunk.world_position; + + // for local_x in 0..CHUNK_SIZE { + // for local_z in 0..CHUNK_SIZE { + // let world_x = chunk_x * CHUNK_SIZE as f64 + local_x as f64; + // let world_z = chunk_z * CHUNK_SIZE as f64 + local_z as f64; + + // let mut noise_value = self.noise.get_noise_2d(world_x as f32, world_z as f32); + // noise_value *= 2f32; + // let height = ((noise_value + 1.0) * 0.5 * self.terrain_height as f32) as i32; + + // for local_y in 0..=height.min(CHUNK_SIZE - 1) { + // let world_y = chunk_y as f64 * CHUNK_SIZE as f64 + local_y as f64; + + // let voxel = if world_y < height as f64 - 3.0 { + // Voxel::Stone + // } else if world_y < height as f64 - 1.0 { + // Voxel::Dirt + // } else if world_y <= height as f64 { + // Voxel::Grass + // } else if world_y < height as f64 + 2.0 { + // Voxel::Water + // } else { + // Voxel::Grass + // }; + // chunk.set_voxel( + // Vector3i::new(local_x, local_y, local_z), + // true, + // Vector3i { + // x: 32, + // y: 32, + // z: 32, + // }, + // ); + // } + // } + // } + // } + // + fn generate_chunk(&self, chunk: &mut Chunk) { + let (chunk_x, chunk_y, chunk_z) = chunk.world_position; + let chunk_world_y = chunk_y * self.chunk_size.y as f64; + + // Early exit: check if entire chunk is above or below terrain + let chunk_min_y = chunk_world_y as f32; + let chunk_max_y = (chunk_world_y + self.chunk_size.y as f64) as f32; + + // Pre-calculate all base heights for this chunk (32x32 = 1024 calculations instead of 32768) + let mut height_cache = [[0.0f32; 32]; 32]; + for local_x in 0..self.chunk_size.x { + let world_x = (chunk_x * self.chunk_size.x as f64 + local_x as f64) as f32; + for local_z in 0..self.chunk_size.z { + let world_z = (chunk_z * self.chunk_size.z as f64 + local_z as f64) as f32; + height_cache[local_x as usize][local_z as usize] = + self.get_base_height(world_x, world_z); + } + } + + // Check if we can fill entire chunk + let max_height = height_cache + .iter() + .flat_map(|row| row.iter()) + .cloned() + .fold(f32::MIN, f32::max); + if chunk_max_y < max_height - 10.0 { + // Entire chunk is underground - fill it + chunk.fill(true); + return; + } + + let min_height = height_cache + .iter() + .flat_map(|row| row.iter()) + .cloned() + .fold(f32::MAX, f32::min); + if chunk_min_y > min_height + 10.0 { + // Entire chunk is above terrain - leave as air + return; + } + + // Generate voxels with cached heights + for local_x in 0..self.chunk_size.x { + let world_x = (chunk_x * self.chunk_size.x as f64 + local_x as f64) as f32; + for local_z in 0..self.chunk_size.z { + let world_z = (chunk_z * self.chunk_size.z as f64 + local_z as f64) as f32; + let base_height = height_cache[local_x as usize][local_z as usize]; + + for local_y in 0..self.chunk_size.y { + let world_y = (chunk_world_y + local_y as f64) as f32; + let relative_height = world_y - base_height; + + // Use 3D noise for density only when needed + let is_solid = if relative_height < -3.0 { + // Deep underground - check for caves + let cave_noise = self.noise_caves.get_noise_3d(world_x, world_y, world_z); + cave_noise < 0.6 // Creates cave systems + } else if relative_height < 0.0 { + true // Just below surface - always solid + } else if relative_height < 15.0 { + let density_value = self.noise.get_noise_3d(world_x, world_y, world_z); + self.should_be_solid(density_value, relative_height) + } else { + false // Above terrain + }; + + if is_solid { + chunk.set_voxel( + Vector3i::new(local_x, local_y, local_z), + true, + self.chunk_size, + ); + } + } + } + } + } + + fn get_base_height(&self, x: f32, z: f32) -> f32 { + // Main terrain with multiple octaves (FBm already applied) + let height_value = self.noise.get_noise_2d(x, z); + + // Add detail noise for small features + let detail = self.noise_detail.get_noise_2d(x, z) * 0.15; + + // Combine and scale - creates hills and valleys like Minecraft + let combined = height_value + detail; + let height = (combined + 1.0) * 0.5 * self.terrain_height as f32; + + // Add some exponential scaling for more dramatic terrain + height.powf(1.3) + } + + fn should_be_solid(&self, density: f32, relative_height: f32) -> bool { + // Base terrain is solid below surface + if relative_height < 0.0 { + return true; + } + + // Create overhangs and floating islands + let cave_threshold = 0.35; // Higher = fewer caves + if relative_height < 15.0 && density > cave_threshold { + return true; + } + + false + } + + fn get_name(&self) -> &str { + "HeightmapGenerator" + } +} diff --git a/src/generation/layered_generator.rs b/src/generation/layered_generator.rs new file mode 100644 index 0000000..75e8c03 --- /dev/null +++ b/src/generation/layered_generator.rs @@ -0,0 +1,39 @@ +use fastnoise_lite::FastNoiseLite; + +// HIGHTLY EXPERIMENTAL!!! + +pub struct LayeredWorldGenerator { + base_noise: FastNoiseLite, // Large scale terrain + detail_noise: FastNoiseLite, // Medium details + cave_noise: FastNoiseLite, // Cave systems + biome_noise: FastNoiseLite, // Biome distribution + terrain_height: i32, +} + +impl LayeredWorldGenerator { + pub fn new(seed: i64) -> Self { + let mut base_noise = FastNoiseLite::new(); + base_noise.set_seed(Some(seed as i32)); + base_noise.set_frequency(Some(0.001)); // Very low frequency for continents + + let mut detail_noise = FastNoiseLite::new(); + detail_noise.set_seed(Some(seed as i32 + 1)); + detail_noise.set_frequency(Some(0.01)); // Medium frequency for hills + + let mut cave_noise = FastNoiseLite::new(); + cave_noise.set_seed(Some(seed as i32 + 2)); + cave_noise.set_frequency(Some(0.05)); // High frequency for small caves + + let mut biome_noise = FastNoiseLite::new(); + biome_noise.set_seed(Some(seed as i32 + 3)); + biome_noise.set_frequency(Some(0.0005)); // Very low for large biomes + + Self { + base_noise, + detail_noise, + cave_noise, + biome_noise, + terrain_height: 64, + } + } +} diff --git a/src/generation/mod.rs b/src/generation/mod.rs new file mode 100644 index 0000000..9061499 --- /dev/null +++ b/src/generation/mod.rs @@ -0,0 +1,7 @@ +pub mod generator; +pub mod heightmap; +pub mod layered_generator; + +pub use generator::WorldGenerator; +pub use heightmap::HeightmapGenerator; +pub use layered_generator::LayeredWorldGenerator; diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..5f2e340 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,15 @@ +use godot::prelude::*; + +mod voxel; +mod world; + +mod chunk; +mod editor; +mod generation; +mod meshing; +mod rendering; + +struct FastVoxel; + +#[gdextension] +unsafe impl ExtensionLibrary for FastVoxel {} diff --git a/src/meshing/binary_greedy_mesher.rs b/src/meshing/binary_greedy_mesher.rs new file mode 100644 index 0000000..1b7094e --- /dev/null +++ b/src/meshing/binary_greedy_mesher.rs @@ -0,0 +1,317 @@ +use crate::chunk::chunk::CHUNK_SIZE; +use crate::chunk::{Chunk, ChunkMesh}; +use crate::editor::voxel_registry::VoxelRegistry; +use crate::meshing::Mesher; +use godot::prelude::*; + +#[derive(Debug, Clone, Copy)] +pub struct BinaryGreedyMesher; + +#[derive(Debug, Clone, Copy)] +struct GreedyQuad { + x: u32, + y: u32, + w: u32, + h: u32, +} + +impl BinaryGreedyMesher { + pub fn new() -> Self { + Self + } + + pub fn generate_mesh(&self, chunk: &Chunk, mesh: &mut ChunkMesh) { + mesh.vertices.clear(); + mesh.normals.clear(); + mesh.indices.clear(); + + // Generate mesh for each of the 6 faces + self.mesh_face(chunk, mesh, 0); // -X (left) + self.mesh_face(chunk, mesh, 1); // +X (right) + self.mesh_face(chunk, mesh, 2); // -Y (down) + self.mesh_face(chunk, mesh, 3); // +Y (up) + self.mesh_face(chunk, mesh, 4); // -Z (back) + self.mesh_face(chunk, mesh, 5); // +Z (forward) + } + + fn mesh_face(&self, chunk: &Chunk, mesh: &mut ChunkMesh, face_dir: usize) { + let size = CHUNK_SIZE as usize; + + // For each slice perpendicular to the face direction + for axis in 0..size { + // Generate binary mask for this slice + let mut face_mask = [0u32; 32]; + + for row in 0..size { + for col in 0..size { + let (x, y, z) = self.get_coords_for_face(face_dir, axis, row, col); + + if x >= size || y >= size || z >= size { + continue; + } + + let pos = Vector3i::new(x as i32, y as i32, z as i32); + let is_solid = chunk.get_voxel( + pos, + Vector3i { + x: 32, + y: 32, + z: 32, + }, + ); + + // Check if neighbor in face direction is air + let (nx, ny, nz) = self.get_neighbor_coords(face_dir, x, y, z); + let neighbor_is_air = if nx < 0 + || nx >= size as i32 + || ny < 0 + || ny >= size as i32 + || nz < 0 + || nz >= size as i32 + { + true // Outside chunk = air + } else { + !chunk.get_voxel( + Vector3i::new(nx, ny, nz), + Vector3i { + x: 32, + y: 32, + z: 32, + }, + ) + }; + + // Face is visible if this voxel is solid and neighbor is air + if is_solid && neighbor_is_air { + face_mask[row] |= 1 << col; + } + } + } + + // Greedy mesh the binary mask + let quads = self.greedy_mesh_binary_plane(face_mask); + + // Add quads to mesh + for quad in quads { + self.add_quad_to_mesh(mesh, quad, face_dir, axis, chunk); + } + } + } + + #[inline] + fn get_coords_for_face( + &self, + face_dir: usize, + axis: usize, + row: usize, + col: usize, + ) -> (usize, usize, usize) { + match face_dir { + 0 => (axis, col, row), // -X: axis=x, row=z, col=y + 1 => (axis, col, row), // +X: axis=x, row=z, col=y + 2 => (row, axis, col), // -Y: axis=y, row=x, col=z + 3 => (row, axis, col), // +Y: axis=y, row=x, col=z + 4 => (row, col, axis), // -Z: axis=z, row=x, col=y + 5 => (row, col, axis), // +Z: axis=z, row=x, col=y + _ => unreachable!(), + } + } + + #[inline] + fn get_neighbor_coords( + &self, + face_dir: usize, + x: usize, + y: usize, + z: usize, + ) -> (i32, i32, i32) { + let (x, y, z) = (x as i32, y as i32, z as i32); + match face_dir { + 0 => (x - 1, y, z), // -X + 1 => (x + 1, y, z), // +X + 2 => (x, y - 1, z), // -Y + 3 => (x, y + 1, z), // +Y + 4 => (x, y, z - 1), // -Z + 5 => (x, y, z + 1), // +Z + _ => unreachable!(), + } + } + + fn greedy_mesh_binary_plane(&self, mut data: [u32; 32]) -> Vec { + let mut quads = Vec::new(); + let size = CHUNK_SIZE as u32; + + for row in 0..data.len() { + let mut y = 0; + while y < size { + // Find first solid bit (skip zeros) + y += (data[row] >> y).trailing_zeros(); + if y >= size { + break; // Reached end of row + } + + // Count consecutive solid bits (height) + let h = (data[row] >> y).trailing_ones(); + + // Create a mask for these bits + let h_as_mask = if h >= 32 { u32::MAX } else { (1u32 << h) - 1 }; + let mask = h_as_mask << y; + + // Try to expand horizontally + let mut w = 1; + while row + w < size as usize { + // Check if the next row has the same pattern at this position + let next_row_bits = (data[row + w] >> y) & h_as_mask; + if next_row_bits != h_as_mask { + break; // Can't expand further + } + + // Clear the bits we're consuming + data[row + w] &= !mask; + w += 1; + } + + quads.push(GreedyQuad { + x: row as u32, + y, + w: w as u32, + h, + }); + + y += h; + } + } + + quads + } + + fn add_quad_to_mesh( + &self, + mesh: &mut ChunkMesh, + quad: GreedyQuad, + face_dir: usize, + axis: usize, + chunk: &Chunk, + ) { + let base_idx = mesh.vertices.len() as i32; + + // Convert quad coordinates back to 3D positions + let vertices = self.get_quad_vertices(quad, face_dir, axis); + let normal = self.get_face_normal(face_dir); + + // Base color based on face direction with simple directional shading (no per-vertex AO) + let (r, g, b) = match face_dir { + 0 => (0.55, 0.55, 0.55), // -X (left) - darker gray + 1 => (0.65, 0.65, 0.65), // +X (right) - lighter gray + 2 => (0.35, 0.35, 0.35), // -Y (down) - dark gray + 3 => (0.5, 0.8, 0.3), // +Y (up) - grass green (brightest) + 4 => (0.50, 0.50, 0.50), // -Z (back) - medium gray + 5 => (0.60, 0.60, 0.60), // +Z (forward) - lighter gray + _ => (1.0, 1.0, 1.0), + }; + + let color = Color::from_rgb(r, g, b); + + // Add vertices with consistent color (no per-vertex variation) + for vertex in &vertices { + mesh.vertices.push(*vertex); + mesh.normals.push(normal); + mesh.colors.push(color); + } + + // Add indices for two triangles + mesh.indices.push(base_idx); + mesh.indices.push(base_idx + 1); + mesh.indices.push(base_idx + 2); + + mesh.indices.push(base_idx); + mesh.indices.push(base_idx + 2); + mesh.indices.push(base_idx + 3); + } + + fn get_quad_vertices(&self, quad: GreedyQuad, face_dir: usize, axis: usize) -> [Vector3; 4] { + let x = quad.x as f32; + let y = quad.y as f32; + let w = quad.w as f32; + let h = quad.h as f32; + let a = axis as f32; + + match face_dir { + 0 => [ + // -X (left) + Vector3::new(a, y, x), + Vector3::new(a, y + h, x), + Vector3::new(a, y + h, x + w), + Vector3::new(a, y, x + w), + ], + 1 => [ + // +X (right) + Vector3::new(a + 1.0, y, x), + Vector3::new(a + 1.0, y, x + w), + Vector3::new(a + 1.0, y + h, x + w), + Vector3::new(a + 1.0, y + h, x), + ], + 2 => [ + // -Y (down) + Vector3::new(x, a, y), + Vector3::new(x, a, y + h), + Vector3::new(x + w, a, y + h), + Vector3::new(x + w, a, y), + ], + 3 => [ + // +Y (up) + Vector3::new(x, a + 1.0, y), + Vector3::new(x + w, a + 1.0, y), + Vector3::new(x + w, a + 1.0, y + h), + Vector3::new(x, a + 1.0, y + h), + ], + 4 => [ + // -Z (back) + Vector3::new(x, y, a), + Vector3::new(x + w, y, a), + Vector3::new(x + w, y + h, a), + Vector3::new(x, y + h, a), + ], + 5 => [ + // +Z (forward) + Vector3::new(x, y, a + 1.0), + Vector3::new(x, y + h, a + 1.0), + Vector3::new(x + w, y + h, a + 1.0), + Vector3::new(x + w, y, a + 1.0), + ], + _ => unreachable!(), + } + } + + fn get_face_normal(&self, face_dir: usize) -> Vector3 { + match face_dir { + 0 => Vector3::new(-1.0, 0.0, 0.0), // -X + 1 => Vector3::new(1.0, 0.0, 0.0), // +X + 2 => Vector3::new(0.0, -1.0, 0.0), // -Y + 3 => Vector3::new(0.0, 1.0, 0.0), // +Y + 4 => Vector3::new(0.0, 0.0, -1.0), // -Z + 5 => Vector3::new(0.0, 0.0, 1.0), // +Z + _ => unreachable!(), + } + } +} + +impl Mesher for BinaryGreedyMesher { + fn generate_mesh(&self, chunk: &Chunk, mesh: &mut ChunkMesh) { + self.generate_mesh(chunk, mesh); + } + + fn generate_mesh_with_registry( + &self, + chunk: &Chunk, + _registry: &VoxelRegistry, + mesh: &mut ChunkMesh, + ) { + // Binary greedy mesher doesn't use registry (only solid/air) + self.generate_mesh(chunk, mesh); + } + + fn get_name(&self) -> &str { + "BinaryGreedyMesher" + } +} diff --git a/src/meshing/mesher.rs b/src/meshing/mesher.rs new file mode 100644 index 0000000..4f8d2aa --- /dev/null +++ b/src/meshing/mesher.rs @@ -0,0 +1,207 @@ +use crate::chunk::{Chunk, ChunkMesh}; +use crate::editor::voxel_registry::VoxelRegistry; +use godot::prelude::*; + +pub trait Mesher: Send + Sync { + fn generate_mesh(&self, chunk: &Chunk, mesh: &mut ChunkMesh); + fn generate_mesh_with_registry( + &self, + chunk: &Chunk, + registry: &VoxelRegistry, + mesh: &mut ChunkMesh, + ); + fn get_name(&self) -> &str; +} + +pub struct CullingMesher; + +impl CullingMesher { + pub fn new() -> Self { + Self + } +} + +impl Mesher for CullingMesher { + fn generate_mesh_with_registry( + &self, + chunk: &Chunk, + registry: &VoxelRegistry, + mesh: &mut ChunkMesh, + ) { + // panic!("Not implemented for CullingMesher") + self.generate_mesh(chunk, mesh); + } + + fn generate_mesh(&self, chunk: &Chunk, mesh: &mut ChunkMesh) { + mesh.clear(); + + // Simple CullingMesher - generate a quad for each solid voxel face + // This is very basic and should be optimized later with greedy meshing + for x in 0..16 { + for y in 0..16 { + for z in 0..16 { + let voxel = chunk.get_voxel( + Vector3i::new(x, y, z), + Vector3i { + x: 32, + y: 32, + z: 32, + }, + ); + + if !voxel { + continue; + } + + self.add_voxel_to_mesh(x, y, z, voxel, mesh, chunk); + } + } + } + } + + fn get_name(&self) -> &str { + "CullingMesher" + } +} + +impl CullingMesher { + fn add_voxel_to_mesh( + &self, + x: i32, + y: i32, + z: i32, + voxel: bool, + mesh: &mut ChunkMesh, + chunk: &Chunk, + ) { + let color = Color { + r: 255.0, + g: 255.0, + b: 255.0, + a: 255.0, + }; + + // Check each face and add it if the neighbor is air + let directions = [ + (Vector3i::new(1, 0, 0), Vector3::RIGHT), + (Vector3i::new(-1, 0, 0), Vector3::LEFT), + (Vector3i::new(0, 1, 0), Vector3::UP), + (Vector3i::new(0, -1, 0), Vector3::DOWN), + (Vector3i::new(0, 0, 1), Vector3::BACK), + (Vector3i::new(0, 0, -1), Vector3::FORWARD), + ]; + + for (offset, normal) in directions.iter() { + let neighbor_pos = Vector3i::new(x + offset.x, y + offset.y, z + offset.z); + + // If neighbor is out of bounds or air, create a face + if !chunk.is_voxel_within_bounds( + neighbor_pos, + Vector3i { + x: 32, + y: 32, + z: 32, + }, + ) || !chunk.get_voxel( + neighbor_pos, + Vector3i { + x: 32, + y: 32, + z: 32, + }, + ) { + self.add_face(x, y, z, *normal, color, mesh); + } + } + } + + fn add_face( + &self, + x: i32, + y: i32, + z: i32, + normal: Vector3, + color: Color, + mesh: &mut ChunkMesh, + ) { + let base_index = mesh.vertices.len() as i32; + let pos = Vector3::new(x as f32, y as f32, z as f32); + + // Define the 4 vertices of the quad based on face direction + let (v0, v1, v2, v3) = Self::get_face_vertices(normal, pos); + + // Add vertices + mesh.vertices.extend_from_slice(&[v0, v1, v2, v3]); + + // Add normals (all same for the face) + for _ in 0..4 { + mesh.normals.push(normal); + } + + // Add colors + for _ in 0..4 { + mesh.colors.push(color); + } + + // Add indices (two triangles) + mesh.indices.extend_from_slice(&[ + base_index, + base_index + 1, + base_index + 2, + base_index + 2, + base_index + 3, + base_index, + ]); + } + + fn get_face_vertices(normal: Vector3, pos: Vector3) -> (Vector3, Vector3, Vector3, Vector3) { + let half = Vector3::new(0.5, 0.5, 0.5); + let center = pos + half; + + match (normal.x as i32, normal.y as i32, normal.z as i32) { + (1, 0, 0) => ( + // RIGHT + center + Vector3::new(0.5, -0.5, -0.5), + center + Vector3::new(0.5, -0.5, 0.5), + center + Vector3::new(0.5, 0.5, 0.5), + center + Vector3::new(0.5, 0.5, -0.5), + ), + (-1, 0, 0) => ( + // LEFT + center + Vector3::new(-0.5, -0.5, 0.5), + center + Vector3::new(-0.5, -0.5, -0.5), + center + Vector3::new(-0.5, 0.5, -0.5), + center + Vector3::new(-0.5, 0.5, 0.5), + ), + (0, 1, 0) => ( + // UP + center + Vector3::new(-0.5, 0.5, -0.5), + center + Vector3::new(0.5, 0.5, -0.5), + center + Vector3::new(0.5, 0.5, 0.5), + center + Vector3::new(-0.5, 0.5, 0.5), + ), + (0, -1, 0) => ( + // DOWN + center + Vector3::new(-0.5, -0.5, 0.5), + center + Vector3::new(0.5, -0.5, 0.5), + center + Vector3::new(0.5, -0.5, -0.5), + center + Vector3::new(-0.5, -0.5, -0.5), + ), + (0, 0, 1) => ( + // BACK + center + Vector3::new(0.5, -0.5, 0.5), + center + Vector3::new(-0.5, -0.5, 0.5), + center + Vector3::new(-0.5, 0.5, 0.5), + center + Vector3::new(0.5, 0.5, 0.5), + ), + (0, 0, -1) => ( + // FORWARD + center + Vector3::new(-0.5, -0.5, -0.5), + center + Vector3::new(0.5, -0.5, -0.5), + center + Vector3::new(0.5, 0.5, -0.5), + center + Vector3::new(-0.5, 0.5, -0.5), + ), + _ => panic!("wrong face"), // fallback, shouldn't happen + } + } +} diff --git a/src/meshing/mod.rs b/src/meshing/mod.rs new file mode 100644 index 0000000..dfe8d3c --- /dev/null +++ b/src/meshing/mod.rs @@ -0,0 +1,7 @@ +pub mod binary_greedy_mesher; +pub mod mesher; +pub mod textured_mesher; + +pub use binary_greedy_mesher::BinaryGreedyMesher; +pub use mesher::Mesher; +pub use textured_mesher::TexturedMesher; diff --git a/src/meshing/textured_mesher.rs b/src/meshing/textured_mesher.rs new file mode 100644 index 0000000..5e66b11 --- /dev/null +++ b/src/meshing/textured_mesher.rs @@ -0,0 +1,410 @@ +use crate::chunk::chunk::CHUNK_SIZE; +use crate::chunk::{self, Chunk, ChunkMesh}; +use crate::editor::voxel_registry::{VoxelModel, VoxelRegistry}; +use crate::meshing::Mesher; +use godot::prelude::*; + +pub struct TexturedMesher { + face_vertices: [[Vector3; 4]; 6], + face_normals: [Vector3; 6], +} + +impl TexturedMesher { + pub fn new() -> Self { + let face_vertices = Self::generate_face_vertices(); + let face_normals = [ + Vector3::LEFT, // 0 + Vector3::RIGHT, // 1 + Vector3::DOWN, // 2 + Vector3::UP, // 3 + Vector3::BACK, // 4 + Vector3::FORWARD, // 5 + ]; + + Self { + face_vertices, + face_normals, + } + } + + fn generate_face_vertices() -> [[Vector3; 4]; 6] { + [ + // LEFT (X-) + [ + Vector3::new(0.0, 0.0, 1.0), + Vector3::new(0.0, 0.0, 0.0), + Vector3::new(0.0, 1.0, 0.0), + Vector3::new(0.0, 1.0, 1.0), + ], + // RIGHT (X+) + [ + Vector3::new(1.0, 0.0, 0.0), + Vector3::new(1.0, 0.0, 1.0), + Vector3::new(1.0, 1.0, 1.0), + Vector3::new(1.0, 1.0, 0.0), + ], + // DOWN (Y-) + [ + Vector3::new(0.0, 0.0, 1.0), + Vector3::new(1.0, 0.0, 1.0), + Vector3::new(1.0, 0.0, 0.0), + Vector3::new(0.0, 0.0, 0.0), + ], + // UP (Y+) + [ + Vector3::new(0.0, 1.0, 0.0), + Vector3::new(1.0, 1.0, 0.0), + Vector3::new(1.0, 1.0, 1.0), + Vector3::new(0.0, 1.0, 1.0), + ], + // BACK (Z+) + [ + Vector3::new(1.0, 0.0, 1.0), + Vector3::new(0.0, 0.0, 1.0), + Vector3::new(0.0, 1.0, 1.0), + Vector3::new(1.0, 1.0, 1.0), + ], + // FRONT (Z-) + [ + Vector3::new(0.0, 0.0, 0.0), + Vector3::new(1.0, 0.0, 0.0), + Vector3::new(1.0, 1.0, 0.0), + Vector3::new(0.0, 1.0, 0.0), + ], + ] + } + #[inline(always)] + fn is_face_visible(chunk: &Chunk, x: usize, y: usize, z: usize, face: usize) -> bool { + // Convert to i32 for neighbor calculation + let (nx, ny, nz) = match face { + 0 => (x as i32 - 1, y as i32, z as i32), // LEFT + 1 => (x as i32 + 1, y as i32, z as i32), // RIGHT + 2 => (x as i32, y as i32 - 1, z as i32), // DOWN + 3 => (x as i32, y as i32 + 1, z as i32), // UP + 4 => (x as i32, y as i32, z as i32 + 1), // BACK + 5 => (x as i32, y as i32, z as i32 - 1), // FRONT + _ => return false, + }; + + // Check if neighbor is out of bounds or empty + if nx < 0 || nx >= CHUNK_SIZE || ny < 0 || ny >= CHUNK_SIZE || nz < 0 || nz >= CHUNK_SIZE { + return true; + } + + !chunk.get_voxel( + Vector3i::new(nx, ny, nz), + Vector3i { + x: 32, + y: 32, + z: 32, + }, + ) // Visible if neighbor is air (false) + } + + fn add_cube_voxel_to_mesh_fast( + &self, + x: usize, + y: usize, + z: usize, + model: &VoxelModel, // Already borrowed, no binding needed + mesh: &mut ChunkMesh, + chunk: &Chunk, + ) { + let pos = Vector3::new(x as f32, y as f32, z as f32); + + for face in 0..6 { + if !Self::is_face_visible(chunk, x, y, z, face) { + continue; + } + + let base_index = mesh.vertices.len() as i32; + + // Add precomputed vertices + for vertex in &self.face_vertices[face] { + mesh.vertices.push(pos + *vertex); + } + + // Add normals (all same for this face) + let normal = self.face_normals[face]; + for _ in 0..4 { + mesh.normals.push(normal); + } + + // Get UVs - precompute this if possible! + let tile_coord = model.get_tile_for_face(face as i32); + let atlas_size = Vector2i::new(16, 16); // Should come from model + let uvs = self.generate_face_uvs_fast(tile_coord, atlas_size); + mesh.uvs.extend_from_slice(&uvs); + + // Add indices + mesh.indices.extend_from_slice(&[ + base_index, + base_index + 1, + base_index + 2, + base_index + 2, + base_index + 3, + base_index, + ]); + } + } + + #[inline(always)] + fn generate_face_uvs_fast(&self, tile_coord: Vector2i, atlas_size: Vector2i) -> [Vector2; 4] { + let inv_atlas_x = 1.0 / atlas_size.x as f32; + let inv_atlas_y = 1.0 / atlas_size.y as f32; + + let u_min = tile_coord.x as f32 * inv_atlas_x; + let u_max = (tile_coord.x + 1) as f32 * inv_atlas_x; + let v_min = tile_coord.y as f32 * inv_atlas_y; + let v_max = (tile_coord.y + 1) as f32 * inv_atlas_y; + + [ + Vector2::new(u_min, v_max), // bottom-left + Vector2::new(u_max, v_max), // bottom-right + Vector2::new(u_max, v_min), // top-right + Vector2::new(u_min, v_min), // top-left + ] + } + + fn add_voxel_to_mesh( + &self, + x: i32, + y: i32, + z: i32, + model_index: i32, + mesh: &mut ChunkMesh, + chunk: &Chunk, + registry: &VoxelRegistry, + ) { + if let Some(model_gd) = registry.models.get(model_index as usize) { + let model = model_gd.bind(); + + // Skip empty models + if model.is_empty() { + return; + } + + // Only handle cube models for now + if model.is_cube() { + self.add_cube_voxel_to_mesh(x, y, z, &model, mesh, chunk); + } else { + godot_print!("No model found for index: {}", model_index); + } + } + } + + fn add_cube_voxel_to_mesh( + &self, + x: i32, + y: i32, + z: i32, + model: &VoxelModel, + mesh: &mut ChunkMesh, + chunk: &Chunk, + ) { + let directions = [ + (Vector3i::new(1, 0, 0), Vector3::RIGHT, 1), // Right face, index 1 + (Vector3i::new(-1, 0, 0), Vector3::LEFT, 0), // Left face, index 0 + (Vector3i::new(0, 1, 0), Vector3::UP, 3), // Top face, index 3 + (Vector3i::new(0, -1, 0), Vector3::DOWN, 2), // Bottom face, index 2 + (Vector3i::new(0, 0, 1), Vector3::BACK, 4), // Back face, index 4 + (Vector3i::new(0, 0, -1), Vector3::FORWARD, 5), // Front face, index 5 + ]; + + for (offset, normal, face_index) in directions.iter() { + let neighbor_pos = Vector3i::new(x + offset.x, y + offset.y, z + offset.z); + + // If neighbor is out of bounds or not solid, create a face + if !chunk.is_voxel_within_bounds( + neighbor_pos, + Vector3i { + x: 32, + y: 32, + z: 32, + }, + ) || !chunk.get_voxel( + neighbor_pos, + Vector3i { + x: 32, + y: 32, + z: 32, + }, + ) { + self.add_textured_face(x, y, z, *normal, *face_index, model, mesh); + } + } + } + + fn add_textured_face( + &self, + x: i32, + y: i32, + z: i32, + normal: Vector3, + face_index: i32, + model: &VoxelModel, + mesh: &mut ChunkMesh, + ) { + let base_index = mesh.vertices.len() as i32; + let pos = Vector3::new(x as f32, y as f32, z as f32); + + // Get face vertices (same as before) + let (v0, v1, v2, v3) = self.get_face_vertices(normal, pos); + + // Add vertices + mesh.vertices.extend_from_slice(&[v0, v1, v2, v3]); + + // Add normals + let normal_vec = Vector3::new(normal.x as f32, normal.y as f32, normal.z as f32); + for _ in 0..4 { + mesh.normals.push(normal_vec); + } + + // Get texture coordinates for this face + let tile_coord = model.get_tile_for_face(face_index); + + // Get atlas size from the model + let atlas_size = Vector2i::new(16, 16); + + // Generate UV coordinates for this face + let uvs = self.generate_face_uvs(tile_coord, atlas_size); + mesh.uvs.extend_from_slice(&uvs); + + // Add indices (two triangles) + mesh.indices.extend_from_slice(&[ + base_index, + base_index + 1, + base_index + 2, + base_index + 2, + base_index + 3, + base_index, + ]); + } + + fn generate_face_uvs(&self, tile_coord: Vector2i, atlas_size: Vector2i) -> [Vector2; 4] { + let tile_size = Vector2::new(1.0 / atlas_size.x as f32, 1.0 / atlas_size.y as f32); + + let u_min = tile_coord.x as f32 * tile_size.x; + let u_max = (tile_coord.x + 1) as f32 * tile_size.x; + let v_min = tile_coord.y as f32 * tile_size.y; + let v_max = (tile_coord.y + 1) as f32 * tile_size.y; + + // Standard quad UV mapping + // Adjust the order based on your vertex winding + [ + Vector2::new(u_min, v_max), // bottom-left + Vector2::new(u_max, v_max), // bottom-right + Vector2::new(u_max, v_min), // top-right + Vector2::new(u_min, v_min), // top-left + ] + } + + fn get_face_vertices( + &self, + normal: Vector3, + pos: Vector3, + ) -> (Vector3, Vector3, Vector3, Vector3) { + let half = Vector3::new(0.5, 0.5, 0.5); + let center = pos + half; + + match (normal.x as i32, normal.y as i32, normal.z as i32) { + (1, 0, 0) => ( + // RIGHT + center + Vector3::new(0.5, -0.5, -0.5), + center + Vector3::new(0.5, -0.5, 0.5), + center + Vector3::new(0.5, 0.5, 0.5), + center + Vector3::new(0.5, 0.5, -0.5), + ), + (-1, 0, 0) => ( + // LEFT + center + Vector3::new(-0.5, -0.5, 0.5), + center + Vector3::new(-0.5, -0.5, -0.5), + center + Vector3::new(-0.5, 0.5, -0.5), + center + Vector3::new(-0.5, 0.5, 0.5), + ), + (0, 1, 0) => ( + // UP + center + Vector3::new(-0.5, 0.5, -0.5), + center + Vector3::new(0.5, 0.5, -0.5), + center + Vector3::new(0.5, 0.5, 0.5), + center + Vector3::new(-0.5, 0.5, 0.5), + ), + (0, -1, 0) => ( + // DOWN + center + Vector3::new(-0.5, -0.5, 0.5), + center + Vector3::new(0.5, -0.5, 0.5), + center + Vector3::new(0.5, -0.5, -0.5), + center + Vector3::new(-0.5, -0.5, -0.5), + ), + (0, 0, 1) => ( + // BACK + center + Vector3::new(0.5, -0.5, 0.5), + center + Vector3::new(-0.5, -0.5, 0.5), + center + Vector3::new(-0.5, 0.5, 0.5), + center + Vector3::new(0.5, 0.5, 0.5), + ), + (0, 0, -1) => ( + // FORWARD + center + Vector3::new(-0.5, -0.5, -0.5), + center + Vector3::new(0.5, -0.5, -0.5), + center + Vector3::new(0.5, 0.5, -0.5), + center + Vector3::new(-0.5, 0.5, -0.5), + ), + _ => panic!("wrong face"), // fallback, shouldn't happen + } + } +} + +impl Mesher for TexturedMesher { + fn generate_mesh(&self, chunk: &Chunk, mesh: &mut ChunkMesh) { + panic!("TexturedMesher requires a VoxelRegistry. Use generate_mesh_with_registry instead."); + } + + fn generate_mesh_with_registry( + &self, + chunk: &Chunk, + registry: &VoxelRegistry, + mesh: &mut ChunkMesh, + ) { + mesh.clear(); + + let estimated_faces = 4096 / 4 * 3; // ~3000 faces + mesh.vertices.reserve(estimated_faces * 4); + mesh.normals.reserve(estimated_faces * 4); + mesh.uvs.reserve(estimated_faces * 4); + mesh.indices.reserve(estimated_faces * 6); + + for x in 0..32 { + for y in 0..32 { + for z in 0..32 { + let is_solid = chunk.get_voxel( + Vector3i::new(x, y, z), + Vector3i { + x: 32, + y: 32, + z: 32, + }, + ); + if !is_solid { + continue; + } + // For now, use model index 1 for all solid voxels + // In the future, you might want to store material types separately + let model = registry.models.at(1); + self.add_cube_voxel_to_mesh_fast( + x as usize, + y as usize, + z as usize, + &model.bind(), + mesh, + chunk, + ); + } + } + } + } + + fn get_name(&self) -> &str { + "TexturedMesher" + } +} diff --git a/src/rendering/mod.rs b/src/rendering/mod.rs new file mode 100644 index 0000000..08f333b --- /dev/null +++ b/src/rendering/mod.rs @@ -0,0 +1,3 @@ +pub mod renderer; + +pub use renderer::Renderer; diff --git a/src/rendering/renderer.rs b/src/rendering/renderer.rs new file mode 100644 index 0000000..69a8b59 --- /dev/null +++ b/src/rendering/renderer.rs @@ -0,0 +1,220 @@ +use std::{collections::HashMap, time::Instant}; + +use crate::chunk::{self, Chunk, ChunkMesh}; +use godot::{ + classes::{ + ArrayMesh, Material, MeshInstance3D, ResourceLoader, RenderingServer, StandardMaterial3D, + base_material_3d::{CullMode, Flags, ShadingMode, Transparency}, + geometry_instance_3d::ShadowCastingSetting, + mesh::{ArrayType, PrimitiveType}, + rendering_server::InstanceType, + }, + obj::IndexEnum, + prelude::*, +}; + +pub struct Renderer { + mesh_instances: HashMap<(i32, i32, i32), Gd>, + terrain_material: Option>, + // Cache for reusable mesh instances + mesh_instance_pool: Vec>, +} + +impl Renderer { + pub fn new() -> Self { + let terrain_material = Self::load_terrain_material(); + Self { + mesh_instances: HashMap::new(), + terrain_material, + mesh_instance_pool: Vec::with_capacity(100), + } + } + + #[inline] + fn load_terrain_material() -> Option> { + let mut resource_loader = ResourceLoader::singleton(); + + let path: GString = "res://materials/terrain_material.tres".into(); + resource_loader + .load(&path) + .and_then(|resource| resource.try_cast::().ok()) + } + + pub fn render_chunk(&mut self, chunk: &Chunk, mesh: &ChunkMesh) -> Gd { + let chunk_key = chunk.world_position; + let chunk_key_i32 = (chunk_key.0 as i32, chunk_key.1 as i32, chunk_key.2 as i32); + + if let Some(old_instance) = self.mesh_instances.remove(&chunk_key_i32) { + old_instance.free(); + godot_print!("Renderer: Replaced mesh instance at {:?}", chunk_key); + } + + // Skip empty meshes + if mesh.is_empty() { + return MeshInstance3D::new_alloc(); + } + + // Create Godot arrays for mesh data - use new() instead of new_gd() + let mut array_mesh = ArrayMesh::new_gd(); + array_mesh.clear_surfaces(); + + let mut surface_array: VariantArray = VariantArray::new(); + + // Convert our mesh data to Godot arrays + surface_array.resize(ArrayType::MAX.to_index(), &Variant::nil()); // Mesh::ARRAY_MAX + + // Vertices - push directly without to_variant() + let mut vertices = PackedVector3Array::new(); + for vertex in &mesh.vertices { + vertices.push(*vertex); + } + surface_array.set(ArrayType::VERTEX.to_index(), &Variant::from(vertices)); // Remove .to_variant() + + // Normals + let mut normals = PackedVector3Array::new(); + for normal in &mesh.normals { + normals.push(*normal); + } + surface_array.set(ArrayType::NORMAL.to_index(), &Variant::from(normals)); // Remove .to_variant() + + // Colors + let mut colors = PackedColorArray::new(); + for color in &mesh.colors { + colors.push(*color); + } + surface_array.set(ArrayType::COLOR.to_index(), &Variant::from(colors)); // Remove .to_variant() + + // Indices + let mut indices = PackedInt32Array::new(); + for index in &mesh.indices { + indices.push(*index); + } + surface_array.set(ArrayType::INDEX.to_index(), &Variant::from(indices)); // Remove .to_variant() + + // Create the mesh - use correct arguments + array_mesh.add_surface_from_arrays(PrimitiveType::TRIANGLES, &surface_array); + + // Create mesh instance using new_alloc() for manually managed nodes + let mut mesh_instance = MeshInstance3D::new_alloc(); + mesh_instance.set_mesh(&array_mesh); + + // Position the mesh instance at the chunk's world position + let (x, y, z) = chunk.world_position; + mesh_instance.set_position(Vector3::new( + (x * 16 as f64) as f32, + (y * 16 as f64) as f32, + (z * 16 as f64) as f32, + )); + + // Create a proper material that supports shadows + let mut material = StandardMaterial3D::new_gd(); + + // Enable shadows + material.set_flag(Flags::ALBEDO_FROM_VERTEX_COLOR, true); + material.set_shading_mode(ShadingMode::PER_PIXEL); + material.set_transparency(Transparency::DISABLED); + material.set_cull_mode(CullMode::BACK); + + // You can also add some basic properties + material.set_roughness(0.8); + material.set_metallic(0.0); + + // Create mesh instance + mesh_instance.set_material_override(&material); + + // Enable shadows for the mesh instance + mesh_instance.set_cast_shadows_setting(ShadowCastingSetting::ON); + + self.mesh_instances + .insert(chunk_key_i32, mesh_instance.clone()); + + mesh_instance + } + + pub fn render_chunk_with_uvs(&mut self, chunk: &Chunk, mesh: &ChunkMesh) -> Gd { + let chunk_key = chunk.world_position; + let chunk_key_i32 = (chunk_key.0 as i32, chunk_key.1 as i32, chunk_key.2 as i32); + + // Reuse or remove old instance + if let Some(old_instance) = self.mesh_instances.remove(&chunk_key_i32) { + // Return to pool instead of freeing + self.mesh_instance_pool.push(old_instance); + } + + if mesh.is_empty() { + return MeshInstance3D::new_alloc(); + } + + // Create mesh arrays more efficiently + let mut array_mesh = ArrayMesh::new_gd(); + let mut surface_array: VariantArray = VariantArray::new(); + surface_array.resize(ArrayType::MAX.to_index(), &Variant::nil()); + + // Use push instead of set for better performance + let mut vertices = PackedVector3Array::new(); + for vertex in &mesh.vertices { + vertices.push(*vertex); + } + surface_array.set(ArrayType::VERTEX.to_index(), &Variant::from(vertices)); + + let mut normals = PackedVector3Array::new(); + for normal in &mesh.normals { + normals.push(*normal); + } + surface_array.set(ArrayType::NORMAL.to_index(), &Variant::from(normals)); + + let mut uvs = PackedVector2Array::new(); + for uv in &mesh.uvs { + uvs.push(*uv); + } + surface_array.set(ArrayType::TEX_UV.to_index(), &Variant::from(uvs)); + + let mut indices = PackedInt32Array::new(); + for index in &mesh.indices { + indices.push(*index); + } + surface_array.set(ArrayType::INDEX.to_index(), &Variant::from(indices)); + + array_mesh.add_surface_from_arrays(PrimitiveType::TRIANGLES, &surface_array); + + // Reuse mesh instance from pool or create new + let mut mesh_instance = self.mesh_instance_pool.pop().unwrap_or_else(|| { + let mut instance = MeshInstance3D::new_alloc(); + instance.set_cast_shadows_setting(ShadowCastingSetting::ON); + instance + }); + + mesh_instance.set_mesh(&array_mesh); + + // Position the mesh instance (chunk size is now 32) + let (x, y, z) = chunk.world_position; + mesh_instance.set_position(Vector3::new( + (x * 32.0) as f32, + (y * 32.0) as f32, + (z * 32.0) as f32, + )); + + // Apply material if available (do this without timing) + if let Some(mat) = &self.terrain_material { + mesh_instance.set_material_override(mat); + } + + self.mesh_instances + .insert(chunk_key_i32, mesh_instance.clone()); + + mesh_instance + } + + pub fn clear(&mut self) { + for (_, instance) in self.mesh_instances.drain() { + self.mesh_instance_pool.push(instance); + } + self.mesh_instances.clear(); + } + + pub fn remove_chunk(&mut self, chunk_position: (i32, i32, i32)) { + if let Some(instance) = self.mesh_instances.remove(&chunk_position) { + self.mesh_instance_pool.push(instance); + } + } +} diff --git a/src/voxel/mod.rs b/src/voxel/mod.rs new file mode 100644 index 0000000..5351b9f --- /dev/null +++ b/src/voxel/mod.rs @@ -0,0 +1,7 @@ +pub mod model; +pub mod registry; +pub mod voxel; + +pub use model::*; +pub use registry::VoxelRegistry; +pub use voxel::Voxel; diff --git a/src/voxel/model.rs b/src/voxel/model.rs new file mode 100644 index 0000000..e2e6703 --- /dev/null +++ b/src/voxel/model.rs @@ -0,0 +1,24 @@ +use godot::classes::class_macros::private::virtuals::Os::Vector2i; + +#[derive(Debug, PartialEq, Clone, Copy)] +#[repr(u8)] +pub enum VoxelModelType { + Empty = 0_u8, + Cube, + Mesh, + Fluid, +} + +#[derive(Debug, Clone, Copy)] +pub struct VoxelModel { + voxel_type: VoxelModelType, + + atlas_size_in_tiles: Vector2i, + + tile_left: Vector2i, + tile_right: Vector2i, + tile_bottom: Vector2i, + tile_top: Vector2i, + tile_back: Vector2i, + tile_front: Vector2i, +} diff --git a/src/voxel/registry.rs b/src/voxel/registry.rs new file mode 100644 index 0000000..4130f28 --- /dev/null +++ b/src/voxel/registry.rs @@ -0,0 +1,13 @@ +use crate::voxel::VoxelModel; + +pub struct VoxelRegistry { + pub models: Vec, +} + +impl VoxelRegistry { + pub fn new(size: usize) -> Self { + VoxelRegistry { + models: Vec::with_capacity(size), + } + } +} diff --git a/src/voxel/voxel.rs b/src/voxel/voxel.rs new file mode 100644 index 0000000..cabe88d --- /dev/null +++ b/src/voxel/voxel.rs @@ -0,0 +1,33 @@ +use godot::classes::class_macros::private::virtuals::Os::Color; + +#[derive(Copy, Clone, Debug, PartialEq, Default)] +pub enum Voxel { + #[default] + Air = 0, + Grass, + Dirt, + Stone, + Sand, + Water, +} + +impl Voxel { + pub fn get_color(&self) -> Color { + match self { + Voxel::Air => Color::from_rgba(0.0, 0.0, 0.0, 0.0), // Transparent + Voxel::Grass => Color::from_rgb(0.2, 0.8, 0.2), + Voxel::Dirt => Color::from_rgb(0.5, 0.3, 0.1), + Voxel::Stone => Color::from_rgb(0.4, 0.4, 0.4), + Voxel::Sand => Color::from_rgb(0.9, 0.8, 0.5), + Voxel::Water => Color::from_rgba(0.2, 0.4, 0.8, 0.7), + } + } + + pub fn is_solid(&self) -> bool { + match self { + Voxel::Air => false, + Voxel::Water => false, // Or true if you want solid water + _ => true, + } + } +} diff --git a/src/world.rs b/src/world.rs new file mode 100644 index 0000000..3bc7fc9 --- /dev/null +++ b/src/world.rs @@ -0,0 +1,176 @@ +use godot::classes::{INode3D, VoxelGi}; +use godot::prelude::*; + +use crate::chunk::ChunkManager; +use crate::chunk::chunk::CHUNK_SIZE; +use crate::editor::VoxelRegistry; +use crate::generation::{HeightmapGenerator, WorldGenerator}; +use crate::meshing::mesher::CullingMesher; +use crate::meshing::{BinaryGreedyMesher, Mesher}; + +#[derive(GodotClass)] +#[class(base=Node3D,tool)] +struct World { + base: Base, + + // ===== RENDERING GROUP ===== + #[export_group(name = "Rendering")] + #[export] + chunk_size: Vector3i, + + #[export(range = (2.0,64.0))] + render_distance: u8, + + // ===== GENERATION GROUP ===== + #[export_group(name = "Generation")] + #[export] + world_seed: i32, + + #[export(range = (0.01, 4.0, 0.01))] + noise_frequency: f32, + + #[export(range = (1.0, 10.0))] + terrain_height: u32, + + #[export(range = (1.0, 64.0, 1.0))] + workder_threads: u8, + + #[export_group(name = "Voxel Registry")] + #[export] + registry: Option>, + + #[export_group(name = "GI")] + #[export] + voxelgi_node: Option>, + + // World data (temporary) + chunk_manager: ChunkManager, + generator: Box, + mesher: Box, +} + +#[godot_api] +impl INode3D for World { + fn init(base: Base) -> Self { + godot_print!("🧊 Hello from FastVoxel"); + + let generator = Box::new(HeightmapGenerator::new(1234, 0.03, 5)); + let mesher = Box::new(CullingMesher::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: 5, + base, + 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; + } + + godot_print!("ChunkSize is {0}", self.chunk_size); + self.generator = Box::new(HeightmapGenerator::new( + self.get_world_seed() as i64, + self.noise_frequency, + self.terrain_height as i32, + )); + 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 x in -distance..=distance { + for z in -distance..=distance { + self.chunk_manager.generate_chunk_column( + x, + z, + &library_ref, + self.generator.as_ref(), + self.mesher.as_ref(), + ); + } + } + godot_print!("Textured terrain regenerated!"); + } + } + } + + fn regenerate_terrain(&mut self) { + godot_print!("Regenerating terrain..."); + + self.chunk_manager.clear(); + + // Create textured mesher + self.mesher = Box::new(CullingMesher::new()); + + self.generate_terrain(); + } + + #[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.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::()); + } + } +}