messy state
This commit is contained in:
108
src/chunk/chunk.rs
Normal file
108
src/chunk/chunk.rs
Normal file
@@ -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<u32>,
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
229
src/chunk/chunk_manager.rs
Normal file
229
src/chunk/chunk_manager.rs
Normal file
@@ -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<Gd<MeshInstance3D>>,
|
||||
}
|
||||
|
||||
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<Gd<MeshInstance3D>> {
|
||||
std::mem::take(&mut self.pending_mesh_instances)
|
||||
}
|
||||
}
|
||||
89
src/chunk/column.rs
Normal file
89
src/chunk/column.rs
Normal file
@@ -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<Chunk>; 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
|
||||
}
|
||||
}
|
||||
}
|
||||
40
src/chunk/mesh.rs
Normal file
40
src/chunk/mesh.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
use godot::prelude::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ChunkMesh {
|
||||
pub vertices: Vec<Vector3>,
|
||||
pub normals: Vec<Vector3>,
|
||||
pub colors: Vec<Color>,
|
||||
pub uvs: Vec<Vector2>,
|
||||
pub indices: Vec<i32>,
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
9
src/chunk/mod.rs
Normal file
9
src/chunk/mod.rs
Normal file
@@ -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;
|
||||
3
src/editor/mod.rs
Normal file
3
src/editor/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod voxel_registry;
|
||||
|
||||
pub use voxel_registry::VoxelRegistry;
|
||||
95
src/editor/voxel_registry.rs
Normal file
95
src/editor/voxel_registry.rs
Normal file
@@ -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<Gd<VoxelModel>>,
|
||||
|
||||
base: Base<Resource>,
|
||||
}
|
||||
|
||||
#[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),
|
||||
}
|
||||
}
|
||||
}
|
||||
8
src/generation/generator.rs
Normal file
8
src/generation/generator.rs
Normal file
@@ -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;
|
||||
}
|
||||
202
src/generation/heightmap.rs
Normal file
202
src/generation/heightmap.rs
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
39
src/generation/layered_generator.rs
Normal file
39
src/generation/layered_generator.rs
Normal file
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
7
src/generation/mod.rs
Normal file
7
src/generation/mod.rs
Normal file
@@ -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;
|
||||
15
src/lib.rs
Normal file
15
src/lib.rs
Normal file
@@ -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 {}
|
||||
317
src/meshing/binary_greedy_mesher.rs
Normal file
317
src/meshing/binary_greedy_mesher.rs
Normal file
@@ -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<GreedyQuad> {
|
||||
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"
|
||||
}
|
||||
}
|
||||
207
src/meshing/mesher.rs
Normal file
207
src/meshing/mesher.rs
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
7
src/meshing/mod.rs
Normal file
7
src/meshing/mod.rs
Normal file
@@ -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;
|
||||
410
src/meshing/textured_mesher.rs
Normal file
410
src/meshing/textured_mesher.rs
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
3
src/rendering/mod.rs
Normal file
3
src/rendering/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod renderer;
|
||||
|
||||
pub use renderer::Renderer;
|
||||
220
src/rendering/renderer.rs
Normal file
220
src/rendering/renderer.rs
Normal file
@@ -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<MeshInstance3D>>,
|
||||
terrain_material: Option<Gd<Material>>,
|
||||
// Cache for reusable mesh instances
|
||||
mesh_instance_pool: Vec<Gd<MeshInstance3D>>,
|
||||
}
|
||||
|
||||
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<Gd<Material>> {
|
||||
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::<Material>().ok())
|
||||
}
|
||||
|
||||
pub fn render_chunk(&mut self, chunk: &Chunk, mesh: &ChunkMesh) -> Gd<MeshInstance3D> {
|
||||
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<MeshInstance3D> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
7
src/voxel/mod.rs
Normal file
7
src/voxel/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
pub mod model;
|
||||
pub mod registry;
|
||||
pub mod voxel;
|
||||
|
||||
pub use model::*;
|
||||
pub use registry::VoxelRegistry;
|
||||
pub use voxel::Voxel;
|
||||
24
src/voxel/model.rs
Normal file
24
src/voxel/model.rs
Normal file
@@ -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,
|
||||
}
|
||||
13
src/voxel/registry.rs
Normal file
13
src/voxel/registry.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
use crate::voxel::VoxelModel;
|
||||
|
||||
pub struct VoxelRegistry {
|
||||
pub models: Vec<VoxelModel>,
|
||||
}
|
||||
|
||||
impl VoxelRegistry {
|
||||
pub fn new(size: usize) -> Self {
|
||||
VoxelRegistry {
|
||||
models: Vec::with_capacity(size),
|
||||
}
|
||||
}
|
||||
}
|
||||
33
src/voxel/voxel.rs
Normal file
33
src/voxel/voxel.rs
Normal file
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
176
src/world.rs
Normal file
176
src/world.rs
Normal file
@@ -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<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,
|
||||
generator: Box<dyn WorldGenerator>,
|
||||
mesher: Box<dyn Mesher>,
|
||||
}
|
||||
|
||||
#[godot_api]
|
||||
impl INode3D for World {
|
||||
fn init(base: Base<Node3D>) -> 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::<Node3D>());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user