Readded old voxel system
@ -14,4 +14,7 @@ rand = "0.8.5"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
toml = "0.8"
|
||||
big_space = "0.9.1"
|
||||
noise = "0.9.0"
|
||||
noise = "0.9.0"
|
||||
itertools = "0.13.0"
|
||||
bitvec = "1.0.1"
|
||||
smallvec = "1.14.0"
|
||||
3
client/assets/textures/generate_skybox.bat
Normal file
@ -0,0 +1,3 @@
|
||||
#https://github.com/KhronosGroup/KTX-Software/releases
|
||||
|
||||
toktx --t2 --cubemap --target_type RGBA --zcmp 18 --genmipmap sky.ktx2 right.jpg left.jpg top.jpg bottom.jpg front.jpg back.jpg
|
||||
BIN
client/assets/textures/skybox_landscape/back.jpg
Normal file
|
After Width: | Height: | Size: 723 KiB |
BIN
client/assets/textures/skybox_landscape/bottom.jpg
Normal file
|
After Width: | Height: | Size: 274 KiB |
BIN
client/assets/textures/skybox_landscape/front.jpg
Normal file
|
After Width: | Height: | Size: 462 KiB |
BIN
client/assets/textures/skybox_landscape/left.jpg
Normal file
|
After Width: | Height: | Size: 588 KiB |
BIN
client/assets/textures/skybox_landscape/right.jpg
Normal file
|
After Width: | Height: | Size: 525 KiB |
BIN
client/assets/textures/skybox_landscape/sky.ktx2
Normal file
BIN
client/assets/textures/skybox_landscape/top.jpg
Normal file
|
After Width: | Height: | Size: 338 KiB |
BIN
client/assets/textures/skybox_space/back.png
Normal file
|
After Width: | Height: | Size: 4.7 MiB |
BIN
client/assets/textures/skybox_space/bottom.png
Normal file
|
After Width: | Height: | Size: 5.2 MiB |
BIN
client/assets/textures/skybox_space/front.png
Normal file
|
After Width: | Height: | Size: 4.9 MiB |
BIN
client/assets/textures/skybox_space/left.png
Normal file
|
After Width: | Height: | Size: 4.7 MiB |
BIN
client/assets/textures/skybox_space/right.png
Normal file
|
After Width: | Height: | Size: 5.2 MiB |
BIN
client/assets/textures/skybox_space/sky.ktx2
Normal file
BIN
client/assets/textures/skybox_space/top.png
Normal file
|
After Width: | Height: | Size: 4.0 MiB |
BIN
client/assets/textures/skybox_space_1024/sky.ktx2
Normal file
@ -7,12 +7,11 @@ use big_space::prelude::GridCell;
|
||||
use noise::{Fbm, NoiseFn, Perlin};
|
||||
use crate::plugins::big_space::big_space_plugin::RootGrid;
|
||||
use crate::plugins::environment::systems::camera_system::CameraController;
|
||||
use crate::plugins::environment::systems::planet_system::PlanetMaker;
|
||||
use crate::plugins::environment::systems::voxels::structure::*;
|
||||
|
||||
pub fn setup(
|
||||
mut commands: Commands,
|
||||
mut meshes: ResMut<Assets<Mesh>>,
|
||||
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||
root: Res<RootGrid>,
|
||||
) {
|
||||
let unit_size = 1.0;
|
||||
@ -27,13 +26,15 @@ pub fn setup(
|
||||
let color = Color::rgb(0.2, 0.8, 0.2);
|
||||
/*generate_voxel_rect(&mut octree,color);*/
|
||||
generate_voxel_sphere(&mut octree, 10, color);
|
||||
|
||||
commands.spawn(
|
||||
(
|
||||
Transform::default(),
|
||||
octree
|
||||
)
|
||||
);
|
||||
|
||||
commands.entity(root.0).with_children(|parent| {
|
||||
parent.spawn(
|
||||
(
|
||||
Transform::default(),
|
||||
octree
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn generate_voxel_sphere(
|
||||
|
||||
145
client/src/plugins/environment/systems/voxels/debug.rs
Normal file
@ -0,0 +1,145 @@
|
||||
use bevy::prelude::*;
|
||||
use crate::plugins::environment::systems::voxels::structure::*;
|
||||
|
||||
/// Visualize each node of the octree as a scaled cuboid, **center-based**.
|
||||
/// `octree_tf.translation` is the world-space center of the root bounding box.
|
||||
pub fn visualize_octree_system(
|
||||
mut gizmos: Gizmos,
|
||||
octree_query: Query<(&SparseVoxelOctree, &Transform)>,
|
||||
) {
|
||||
for (octree, octree_tf) in octree_query.iter() {
|
||||
// The root node covers [-size/2..+size/2], so half_size is:
|
||||
let half_size = octree.size * 0.5;
|
||||
|
||||
// Draw a translucent cuboid for the root
|
||||
gizmos.cuboid(
|
||||
Transform::from_translation(octree_tf.translation)
|
||||
.with_scale(Vec3::splat(octree.size)),
|
||||
Color::rgba(1.0, 1.0, 0.0, 0.15),
|
||||
);
|
||||
|
||||
// Recursively draw children:
|
||||
// Start from depth=0. The node at depth=0 has bounding side = octree.size.
|
||||
visualize_recursive_center(
|
||||
&mut gizmos,
|
||||
&octree.root,
|
||||
octree_tf.translation, // center of root in world
|
||||
octree.size,
|
||||
0,
|
||||
octree.max_depth,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Recursively draws cuboids for each node.
|
||||
/// We follow the same indexing as insert_recursive, i.e. bit patterns:
|
||||
/// i=0 => child in (-x,-y,-z) quadrant,
|
||||
/// i=1 => (+x,-y,-z), i=2 => (-x,+y,-z), etc.
|
||||
fn visualize_recursive_center(
|
||||
gizmos: &mut Gizmos,
|
||||
node: &OctreeNode,
|
||||
parent_center: Vec3,
|
||||
parent_size: f32,
|
||||
depth: u32,
|
||||
max_depth: u32,
|
||||
) {
|
||||
if depth >= max_depth {
|
||||
return;
|
||||
}
|
||||
if let Some(children) = &node.children {
|
||||
// Each child is half the parent’s size
|
||||
let child_size = parent_size * 0.5;
|
||||
let half = child_size * 0.5;
|
||||
|
||||
for (i, child) in children.iter().enumerate() {
|
||||
// For i in [0..8], bits: x=1, y=2, z=4
|
||||
let offset_x = if (i & 1) != 0 { half } else { -half };
|
||||
let offset_y = if (i & 2) != 0 { half } else { -half };
|
||||
let offset_z = if (i & 4) != 0 { half } else { -half };
|
||||
|
||||
let child_center = parent_center + Vec3::new(offset_x, offset_y, offset_z);
|
||||
|
||||
// Draw the child bounding box
|
||||
gizmos.cuboid(
|
||||
Transform::from_translation(child_center).with_scale(Vec3::splat(child_size)),
|
||||
Color::rgba(0.5, 1.0, 0.5, 0.15), // greenish
|
||||
);
|
||||
|
||||
// Recurse
|
||||
visualize_recursive_center(
|
||||
gizmos,
|
||||
child,
|
||||
child_center,
|
||||
child_size,
|
||||
depth + 1,
|
||||
max_depth,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// If node.is_leaf && node.voxel.is_some(), draw a smaller marker
|
||||
if node.is_leaf {
|
||||
if let Some(voxel) = node.voxel {
|
||||
// We'll choose a size that's a fraction of the parent's size.
|
||||
// For example, 25% of the parent bounding box dimension.
|
||||
let leaf_size = parent_size * 0.25;
|
||||
|
||||
// Draw a small cuboid at the same center as the parent node.
|
||||
gizmos.cuboid(
|
||||
Transform::from_translation(parent_center)
|
||||
.with_scale(Vec3::splat(leaf_size)),
|
||||
voxel.color,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn draw_grid(
|
||||
mut gizmos: Gizmos,
|
||||
camera_query: Query<&Transform, With<Camera>>,
|
||||
octree_query: Query<(&SparseVoxelOctree, &Transform)>,
|
||||
) {
|
||||
let camera_tf = camera_query.single();
|
||||
let camera_pos = camera_tf.translation;
|
||||
|
||||
for (octree, octree_tf) in octree_query.iter() {
|
||||
let half_size = octree.size * 0.5;
|
||||
let root_center = octree_tf.translation;
|
||||
|
||||
// Voxel spacing at max depth
|
||||
let spacing = octree.get_spacing_at_depth(octree.max_depth);
|
||||
let grid_count = (octree.size / spacing) as i32;
|
||||
|
||||
// We'll define the bounding region as [center-half_size .. center+half_size].
|
||||
// So the min corner is (root_center - half_size).
|
||||
let min_corner = root_center - Vec3::splat(half_size);
|
||||
|
||||
// Draw lines in X & Z directions (like a ground plane).
|
||||
for i in 0..=grid_count {
|
||||
let offset = i as f32 * spacing;
|
||||
|
||||
// 1) line along Z
|
||||
let x = min_corner.x + offset;
|
||||
let z1 = min_corner.z;
|
||||
let z2 = min_corner.z + (grid_count as f32 * spacing);
|
||||
|
||||
let p1 = Vec3::new(x, min_corner.y, z1);
|
||||
let p2 = Vec3::new(x, min_corner.y, z2);
|
||||
|
||||
// offset by -camera_pos for stable Gizmos in large coords
|
||||
let p1_f32 = p1 - camera_pos;
|
||||
let p2_f32 = p2 - camera_pos;
|
||||
gizmos.line(p1_f32, p2_f32, Color::WHITE);
|
||||
|
||||
// 2) line along X
|
||||
let z = min_corner.z + offset;
|
||||
let x1 = min_corner.x;
|
||||
let x2 = min_corner.x + (grid_count as f32 * spacing);
|
||||
|
||||
let p3 = Vec3::new(x1, min_corner.y, z) - camera_pos;
|
||||
let p4 = Vec3::new(x2, min_corner.y, z) - camera_pos;
|
||||
gizmos.line(p3, p4, Color::WHITE);
|
||||
}
|
||||
}
|
||||
}
|
||||
247
client/src/plugins/environment/systems/voxels/helper.rs
Normal file
@ -0,0 +1,247 @@
|
||||
use bevy::prelude::*;
|
||||
use crate::plugins::environment::systems::voxels::structure::*;
|
||||
|
||||
impl SparseVoxelOctree {
|
||||
pub fn ray_intersects_aabb(&self,ray: &Ray, aabb: &AABB) -> bool {
|
||||
let inv_dir = 1.0 / ray.direction;
|
||||
let t1 = (aabb.min - ray.origin) * inv_dir;
|
||||
let t2 = (aabb.max - ray.origin) * inv_dir;
|
||||
|
||||
let t_min = t1.min(t2);
|
||||
let t_max = t1.max(t2);
|
||||
|
||||
let t_enter = t_min.max_element();
|
||||
let t_exit = t_max.min_element();
|
||||
|
||||
t_enter <= t_exit && t_exit >= 0.0
|
||||
}
|
||||
|
||||
|
||||
/// Returns the size of one voxel at the given depth.
|
||||
pub fn get_spacing_at_depth(&self, depth: u32) -> f32 {
|
||||
let effective = depth.min(self.max_depth);
|
||||
self.size / (2_u32.pow(effective)) as f32
|
||||
}
|
||||
|
||||
|
||||
/// Center-based: [-size/2..+size/2]. Shift +half_size => [0..size], floor, shift back.
|
||||
pub fn normalize_to_voxel_at_depth(&self, position: Vec3, depth: u32) -> Vec3 {
|
||||
// Convert world coordinate to normalized [0,1] space.
|
||||
let half_size = self.size * 0.5;
|
||||
// Shift to [0, self.size]
|
||||
let shifted = (position + Vec3::splat(half_size)) / self.size;
|
||||
// Determine the number of voxels along an edge at the given depth.
|
||||
let voxel_count = 2_u32.pow(depth) as f32;
|
||||
// Get the voxel index (as a float) and then compute the center in normalized space.
|
||||
let voxel_index = (shifted * voxel_count).floor();
|
||||
let voxel_center = (voxel_index + Vec3::splat(0.5)) / voxel_count;
|
||||
voxel_center
|
||||
}
|
||||
pub fn denormalize_voxel_center(&self, voxel_center: Vec3) -> Vec3 {
|
||||
let half_size = self.size * 0.5;
|
||||
// Convert the normalized voxel center back to world space.
|
||||
voxel_center * self.size - Vec3::splat(half_size)
|
||||
}
|
||||
|
||||
|
||||
pub fn compute_child_bounds(&self, bounds: &AABB, index: usize) -> AABB {
|
||||
let min = bounds.min;
|
||||
let max = bounds.max;
|
||||
let center = (min + max) / 2.0;
|
||||
|
||||
let x_min = if (index & 1) == 0 { min.x } else { center.x };
|
||||
let x_max = if (index & 1) == 0 { center.x } else { max.x };
|
||||
|
||||
let y_min = if (index & 2) == 0 { min.y } else { center.y };
|
||||
let y_max = if (index & 2) == 0 { center.y } else { max.y };
|
||||
|
||||
let z_min = if (index & 4) == 0 { min.z } else { center.z };
|
||||
let z_max = if (index & 4) == 0 { center.z } else { max.z };
|
||||
|
||||
let child_bounds = AABB {
|
||||
min: Vec3::new(x_min, y_min, z_min),
|
||||
max: Vec3::new(x_max, y_max, z_max),
|
||||
};
|
||||
|
||||
child_bounds
|
||||
}
|
||||
|
||||
pub fn ray_intersects_aabb_with_normal(
|
||||
&self,
|
||||
ray: &Ray,
|
||||
aabb: &AABB,
|
||||
) -> Option<(f32, f32, Vec3)> {
|
||||
// Define a safe inverse function to avoid division by zero.
|
||||
let safe_inv = |d: f32| if d.abs() < 1e-6 { 1e6 } else { 1.0 / d };
|
||||
let inv_dir = Vec3::new(
|
||||
safe_inv(ray.direction.x),
|
||||
safe_inv(ray.direction.y),
|
||||
safe_inv(ray.direction.z),
|
||||
);
|
||||
|
||||
let t1 = (aabb.min - ray.origin) * inv_dir;
|
||||
let t2 = (aabb.max - ray.origin) * inv_dir;
|
||||
|
||||
let tmin = t1.min(t2);
|
||||
let tmax = t1.max(t2);
|
||||
|
||||
let t_enter = tmin.max_element();
|
||||
let t_exit = tmax.min_element();
|
||||
|
||||
if t_enter <= t_exit && t_exit >= 0.0 {
|
||||
let epsilon = 1e-6;
|
||||
let mut normal = Vec3::ZERO;
|
||||
// Determine which face was hit by comparing t_enter to the computed values.
|
||||
if (t_enter - t1.x).abs() < epsilon || (t_enter - t2.x).abs() < epsilon {
|
||||
normal = Vec3::new(if ray.direction.x < 0.0 { 1.0 } else { -1.0 }, 0.0, 0.0);
|
||||
} else if (t_enter - t1.y).abs() < epsilon || (t_enter - t2.y).abs() < epsilon {
|
||||
normal = Vec3::new(0.0, if ray.direction.y < 0.0 { 1.0 } else { -1.0 }, 0.0);
|
||||
} else if (t_enter - t1.z).abs() < epsilon || (t_enter - t2.z).abs() < epsilon {
|
||||
normal = Vec3::new(0.0, 0.0, if ray.direction.z < 0.0 { 1.0 } else { -1.0 });
|
||||
}
|
||||
Some((t_enter, t_exit, normal))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if (x,y,z) is within [-size/2..+size/2].
|
||||
pub fn contains(&self, x: f32, y: f32, z: f32) -> bool {
|
||||
let half_size = self.size / 2.0;
|
||||
let eps = 1e-6;
|
||||
(x >= -half_size - eps && x < half_size + eps)
|
||||
&& (y >= -half_size - eps && y < half_size + eps)
|
||||
&& (z >= -half_size - eps && z < half_size + eps)
|
||||
}
|
||||
|
||||
/// Retrieve a voxel at world coordinates by normalizing and looking up.
|
||||
pub fn get_voxel_at_world_coords(&self, position: Vec3) -> Option<&Voxel> {
|
||||
let aligned = self.normalize_to_voxel_at_depth(position, self.max_depth);
|
||||
self.get_voxel_at(aligned.x, aligned.y, aligned.z)
|
||||
}
|
||||
|
||||
pub fn local_to_world(&self, local_pos: Vec3) -> Vec3 {
|
||||
// Half the total octree size, used to shift the center to the origin.
|
||||
let half_size = self.size * 0.5;
|
||||
// Convert local coordinate to world space:
|
||||
// 1. Subtract 0.5 to center the coordinate at zero (range becomes [-0.5, 0.5])
|
||||
// 2. Multiply by the total size to scale into world units.
|
||||
// 3. Add half_size to shift from a center–based system to one starting at zero.
|
||||
(local_pos - Vec3::splat(0.5)) * self.size + Vec3::splat(half_size)
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// Helper function to recursively traverse the octree to a specific depth.
|
||||
fn get_node_at_depth(
|
||||
node: &OctreeNode,
|
||||
x: f32,
|
||||
y: f32,
|
||||
z: f32,
|
||||
depth: u32,
|
||||
) -> Option<&OctreeNode> {
|
||||
if depth == 0 {
|
||||
return Some(node); // We've reached the desired depth
|
||||
}
|
||||
|
||||
if let Some(ref children) = node.children {
|
||||
// Determine which child to traverse into
|
||||
let epsilon = 1e-6;
|
||||
let index = ((x >= 0.5 - epsilon) as usize)
|
||||
+ ((y >= 0.5 - epsilon) as usize * 2)
|
||||
+ ((z >= 0.5 - epsilon) as usize * 4);
|
||||
|
||||
let adjust_coord = |coord: f32| {
|
||||
if coord >= 0.5 - epsilon {
|
||||
(coord - 0.5) * 2.0
|
||||
} else {
|
||||
coord * 2.0
|
||||
}
|
||||
};
|
||||
|
||||
// Recurse into the correct child
|
||||
Self::get_node_at_depth(
|
||||
&children[index],
|
||||
adjust_coord(x),
|
||||
adjust_coord(y),
|
||||
adjust_coord(z),
|
||||
depth - 1,
|
||||
)
|
||||
} else {
|
||||
None // Node has no children at this depth
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_volume(&self, node: &OctreeNode) -> bool {
|
||||
// Check if this node is a leaf with a voxel
|
||||
if node.is_leaf && node.voxel.is_some() {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If the node has children, recursively check them
|
||||
if let Some(children) = &node.children {
|
||||
for child in children.iter() {
|
||||
if self.has_volume(child) {
|
||||
return true; // If any child has a voxel, the chunk has volume
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no voxel found in this node or its children
|
||||
false
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// Returns the (face_normal, local_offset) for the given neighbor direction.
|
||||
/// - `dx, dy, dz`: The integer direction of the face (-1,0,0 / 1,0,0 / etc.)
|
||||
/// - `voxel_size_f`: The world size of a single voxel (e.g. step as f32).
|
||||
pub fn face_orientation(dx: f32, dy: f32, dz: f32, voxel_size_f: f32) -> (Vec3, Vec3) {
|
||||
// We'll do a match on the direction
|
||||
match (dx, dy, dz) {
|
||||
// Negative X => face normal is (-1, 0, 0), local offset is -voxel_size/2 in X
|
||||
(-1.0, 0.0, 0.0) => {
|
||||
let normal = Vec3::new(-1.0, 0.0, 0.0);
|
||||
let offset = Vec3::new(-voxel_size_f * 0.5, 0.0, 0.0);
|
||||
(normal, offset)
|
||||
}
|
||||
// Positive X
|
||||
(1.0, 0.0, 0.0) => {
|
||||
let normal = Vec3::new(1.0, 0.0, 0.0);
|
||||
let offset = Vec3::new(voxel_size_f * 0.5, 0.0, 0.0);
|
||||
(normal, offset)
|
||||
}
|
||||
// Negative Y
|
||||
(0.0, -1.0, 0.0) => {
|
||||
let normal = Vec3::new(0.0, -1.0, 0.0);
|
||||
let offset = Vec3::new(0.0, -voxel_size_f * 0.5, 0.0);
|
||||
(normal, offset)
|
||||
}
|
||||
// Positive Y
|
||||
(0.0, 1.0, 0.0) => {
|
||||
let normal = Vec3::new(0.0, 1.0, 0.0);
|
||||
let offset = Vec3::new(0.0, voxel_size_f * 0.5, 0.0);
|
||||
(normal, offset)
|
||||
}
|
||||
// Negative Z
|
||||
(0.0, 0.0, -1.0) => {
|
||||
let normal = Vec3::new(0.0, 0.0, -1.0);
|
||||
let offset = Vec3::new(0.0, 0.0, -voxel_size_f * 0.5);
|
||||
(normal, offset)
|
||||
}
|
||||
// Positive Z
|
||||
(0.0, 0.0, 1.0) => {
|
||||
let normal = Vec3::new(0.0, 0.0, 1.0);
|
||||
let offset = Vec3::new(0.0, 0.0, voxel_size_f * 0.5);
|
||||
(normal, offset)
|
||||
}
|
||||
// If the direction is not one of the 6 axis directions, you might skip or handle differently
|
||||
_ => {
|
||||
// For safety, we can panic or return a default.
|
||||
// But typically you won't call face_orientation with an invalid direction
|
||||
panic!("Invalid face direction: ({}, {}, {})", dx, dy, dz);
|
||||
}
|
||||
}
|
||||
}
|
||||
5
client/src/plugins/environment/systems/voxels/mod.rs
Normal file
@ -0,0 +1,5 @@
|
||||
pub mod debug;
|
||||
pub mod helper;
|
||||
pub mod octree;
|
||||
pub mod structure;
|
||||
pub mod rendering;
|
||||
393
client/src/plugins/environment/systems/voxels/octree.rs
Normal file
@ -0,0 +1,393 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use bevy::asset::Assets;
|
||||
use bevy::color::Color;
|
||||
use bevy::math::{DQuat, DVec3};
|
||||
use bevy::prelude::*;
|
||||
use bevy::render::mesh::{Indices, PrimitiveTopology, VertexAttributeValues};
|
||||
use bevy::render::render_asset::RenderAssetUsages;
|
||||
use crate::plugins::environment::systems::voxels::structure::{DirtyVoxel, OctreeNode, Ray, SparseVoxelOctree, Voxel, AABB, NEIGHBOR_OFFSETS};
|
||||
|
||||
impl SparseVoxelOctree {
|
||||
/// Creates a new octree with the specified max depth, size, and wireframe visibility.
|
||||
pub fn new(max_depth: u32, size: f32, show_wireframe: bool, show_world_grid: bool, show_chunks: bool) -> Self {
|
||||
Self {
|
||||
root: OctreeNode::new(),
|
||||
max_depth,
|
||||
size,
|
||||
show_wireframe,
|
||||
show_world_grid,
|
||||
show_chunks,
|
||||
dirty: Vec::new(),
|
||||
}
|
||||
}
|
||||
pub fn insert(&mut self, position: Vec3, voxel: Voxel) {
|
||||
// Align to the center of the voxel at max_depth
|
||||
let mut aligned = self.normalize_to_voxel_at_depth(position, self.max_depth);
|
||||
let mut world_center = self.denormalize_voxel_center(aligned);
|
||||
|
||||
// Expand as needed using the denormalized position.
|
||||
while !self.contains(world_center.x, world_center.y, world_center.z) {
|
||||
self.expand_root(world_center.x, world_center.y, world_center.z);
|
||||
// Recompute aligned and world_center after expansion.
|
||||
aligned = self.normalize_to_voxel_at_depth(position, self.max_depth);
|
||||
world_center = self.denormalize_voxel_center(aligned);
|
||||
}
|
||||
|
||||
let dirty_voxel = DirtyVoxel{
|
||||
position: aligned,
|
||||
};
|
||||
self.dirty.push(dirty_voxel);
|
||||
|
||||
|
||||
Self::insert_recursive(&mut self.root, aligned, voxel, self.max_depth);
|
||||
}
|
||||
|
||||
fn insert_recursive(node: &mut OctreeNode, position: Vec3, voxel: Voxel, depth: u32) {
|
||||
if depth == 0 {
|
||||
node.voxel = Some(voxel);
|
||||
node.is_leaf = true;
|
||||
return;
|
||||
}
|
||||
let epsilon = 1e-6;
|
||||
// Determine octant index by comparing with 0.5
|
||||
let index = ((position.x >= 0.5 - epsilon) as usize)
|
||||
+ ((position.y >= 0.5 - epsilon) as usize * 2)
|
||||
+ ((position.z >= 0.5 - epsilon) as usize * 4);
|
||||
|
||||
// If there are no children, create them.
|
||||
if node.children.is_none() {
|
||||
node.children = Some(Box::new(core::array::from_fn(|_| OctreeNode::new())));
|
||||
node.is_leaf = false;
|
||||
}
|
||||
if let Some(ref mut children) = node.children {
|
||||
// Adjust coordinate into the child’s [0, 1] range.
|
||||
let adjust_coord = |coord: f32| {
|
||||
if coord >= 0.5 - epsilon {
|
||||
(coord - 0.5) * 2.0
|
||||
} else {
|
||||
coord * 2.0
|
||||
}
|
||||
};
|
||||
let child_pos = Vec3::new(
|
||||
adjust_coord(position.x),
|
||||
adjust_coord(position.y),
|
||||
adjust_coord(position.z),
|
||||
);
|
||||
Self::insert_recursive(&mut children[index], child_pos, voxel, depth - 1);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, position: Vec3) {
|
||||
let aligned = self.normalize_to_voxel_at_depth(position, self.max_depth);
|
||||
|
||||
let dirty_voxel = DirtyVoxel{
|
||||
position: aligned,
|
||||
};
|
||||
self.dirty.push(dirty_voxel);
|
||||
|
||||
Self::remove_recursive(&mut self.root, aligned.x, aligned.y, aligned.z, self.max_depth);
|
||||
}
|
||||
|
||||
fn remove_recursive(
|
||||
node: &mut OctreeNode,
|
||||
x: f32,
|
||||
y: f32,
|
||||
z: f32,
|
||||
depth: u32,
|
||||
) -> bool {
|
||||
if depth == 0 {
|
||||
if node.voxel.is_some() {
|
||||
node.voxel = None;
|
||||
node.is_leaf = false;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if node.children.is_none() {
|
||||
return false;
|
||||
}
|
||||
let epsilon = 1e-6;
|
||||
let index = ((x >= 0.5 - epsilon) as usize)
|
||||
+ ((y >= 0.5 - epsilon) as usize * 2)
|
||||
+ ((z >= 0.5 - epsilon) as usize * 4);
|
||||
|
||||
let adjust_coord = |coord: f32| {
|
||||
if coord >= 0.5 - epsilon {
|
||||
(coord - 0.5) * 2.0
|
||||
} else {
|
||||
coord * 2.0
|
||||
}
|
||||
};
|
||||
|
||||
let child = &mut node.children.as_mut().unwrap()[index];
|
||||
let should_prune_child = Self::remove_recursive(
|
||||
child,
|
||||
adjust_coord(x),
|
||||
adjust_coord(y),
|
||||
adjust_coord(z),
|
||||
depth - 1,
|
||||
);
|
||||
|
||||
if should_prune_child {
|
||||
// remove the child node
|
||||
node.children.as_mut().unwrap()[index] = OctreeNode::new();
|
||||
}
|
||||
|
||||
// Check if all children are empty
|
||||
let all_children_empty = node
|
||||
.children
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.all(|child| child.is_empty());
|
||||
|
||||
if all_children_empty {
|
||||
node.children = None;
|
||||
node.is_leaf = true;
|
||||
return node.voxel.is_none();
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
|
||||
fn expand_root(&mut self, _x: f32, _y: f32, _z: f32) {
|
||||
info!("Root expanding ...");
|
||||
// Save the old root and its size.
|
||||
let old_root = std::mem::replace(&mut self.root, OctreeNode::new());
|
||||
let old_size = self.size;
|
||||
|
||||
// Update the octree's size and depth.
|
||||
self.size *= 2.0;
|
||||
self.max_depth += 1;
|
||||
|
||||
// Reinsert each voxel from the old tree.
|
||||
let voxels = Self::collect_voxels_from_node(&old_root, old_size);
|
||||
for (world_pos, voxel, _depth) in voxels {
|
||||
self.insert(world_pos, voxel);
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper: Collect all voxels from a given octree node recursively.
|
||||
/// The coordinate system here assumes the node covers [–old_size/2, +old_size/2] in each axis.
|
||||
fn collect_voxels_from_node(node: &OctreeNode, old_size: f32) -> Vec<(Vec3, Voxel, u32)> {
|
||||
let mut voxels = Vec::new();
|
||||
Self::collect_voxels_recursive(node, -old_size / 2.0, -old_size / 2.0, -old_size / 2.0, old_size, 0, &mut voxels);
|
||||
voxels
|
||||
}
|
||||
|
||||
fn collect_voxels_recursive(
|
||||
node: &OctreeNode,
|
||||
x: f32,
|
||||
y: f32,
|
||||
z: f32,
|
||||
size: f32,
|
||||
depth: u32,
|
||||
out: &mut Vec<(Vec3, Voxel, u32)>,
|
||||
) {
|
||||
if node.is_leaf {
|
||||
if let Some(voxel) = node.voxel {
|
||||
// Compute the center of this node's region.
|
||||
let center = Vec3::new(x + size / 2.0, y + size / 2.0, z + size / 2.0);
|
||||
out.push((center, voxel, depth));
|
||||
}
|
||||
}
|
||||
if let Some(children) = &node.children {
|
||||
let half = size / 2.0;
|
||||
for (i, child) in children.iter().enumerate() {
|
||||
let offset_x = if (i & 1) != 0 { half } else { 0.0 };
|
||||
let offset_y = if (i & 2) != 0 { half } else { 0.0 };
|
||||
let offset_z = if (i & 4) != 0 { half } else { 0.0 };
|
||||
Self::collect_voxels_recursive(child, x + offset_x, y + offset_y, z + offset_z, half, depth + 1, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
pub fn traverse(&self) -> Vec<(Vec3, Color, u32)> {
|
||||
let mut voxels = Vec::new();
|
||||
// Start at the normalized center (0.5, 0.5, 0.5) rather than (0,0,0)
|
||||
Self::traverse_recursive(
|
||||
&self.root,
|
||||
Vec3::splat(0.5), // normalized center of the root cell
|
||||
1.0, // full normalized cell size
|
||||
0,
|
||||
&mut voxels,
|
||||
self,
|
||||
);
|
||||
voxels
|
||||
}
|
||||
|
||||
fn traverse_recursive(
|
||||
node: &OctreeNode,
|
||||
local_center: Vec3,
|
||||
size: f32,
|
||||
depth: u32,
|
||||
out: &mut Vec<(Vec3, Color, u32)>,
|
||||
octree: &SparseVoxelOctree,
|
||||
) {
|
||||
// If a leaf contains a voxel, record its world-space center
|
||||
if node.is_leaf {
|
||||
if let Some(voxel) = node.voxel {
|
||||
out.push((octree.denormalize_voxel_center(local_center), voxel.color, depth));
|
||||
}
|
||||
}
|
||||
|
||||
// If the node has children, subdivide the cell into 8 subcells.
|
||||
if let Some(ref children) = node.children {
|
||||
let offset = size / 4.0; // child center offset from parent center
|
||||
let new_size = size / 2.0; // each child cell's size in normalized space
|
||||
for (i, child) in children.iter().enumerate() {
|
||||
// Compute each axis' offset: use +offset if the bit is set, else -offset.
|
||||
let dx = if (i & 1) != 0 { offset } else { -offset };
|
||||
let dy = if (i & 2) != 0 { offset } else { -offset };
|
||||
let dz = if (i & 4) != 0 { offset } else { -offset };
|
||||
let child_center = local_center + Vec3::new(dx, dy, dz);
|
||||
|
||||
Self::traverse_recursive(child, child_center, new_size, depth + 1, out, octree);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// Retrieve a voxel from the octree if it exists (x,y,z in [-0.5..+0.5] range).
|
||||
pub fn get_voxel_at(&self, x: f32, y: f32, z: f32) -> Option<&Voxel> {
|
||||
Self::get_voxel_recursive(&self.root, x, y, z)
|
||||
}
|
||||
|
||||
fn get_voxel_recursive(node: &OctreeNode, x: f32, y: f32, z: f32) -> Option<&Voxel> {
|
||||
if node.is_leaf {
|
||||
return node.voxel.as_ref();
|
||||
}
|
||||
if let Some(children) = &node.children {
|
||||
let epsilon = 1e-6;
|
||||
let index = ((x >= 0.5 - epsilon) as usize)
|
||||
+ ((y >= 0.5 - epsilon) as usize * 2)
|
||||
+ ((z >= 0.5 - epsilon) as usize * 4);
|
||||
let adjust_coord = |coord: f32| {
|
||||
if coord >= 0.5 - epsilon {
|
||||
(coord - 0.5) * 2.0
|
||||
} else {
|
||||
coord * 2.0
|
||||
}
|
||||
};
|
||||
Self::get_voxel_recursive(
|
||||
&children[index],
|
||||
adjust_coord(x),
|
||||
adjust_coord(y),
|
||||
adjust_coord(z),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if there is a neighbor voxel at the specified direction from the given world coordinates at the specified depth.
|
||||
/// The offsets are directions (-1, 0, 1) for x, y, z.
|
||||
pub fn has_neighbor(
|
||||
&self,
|
||||
position: Vec3,
|
||||
offset_x: i32,
|
||||
offset_y: i32,
|
||||
offset_z: i32,
|
||||
depth: u32,
|
||||
) -> bool {
|
||||
let aligned = self.normalize_to_voxel_at_depth(position, depth);
|
||||
let voxel_count = 2_u32.pow(depth) as f32;
|
||||
// Normalized voxel size is 1/voxel_count
|
||||
let norm_voxel_size = 1.0 / voxel_count;
|
||||
|
||||
let neighbor = Vec3::new(
|
||||
aligned.x + (offset_x as f32) * norm_voxel_size,
|
||||
aligned.y + (offset_y as f32) * norm_voxel_size,
|
||||
aligned.z + (offset_z as f32) * norm_voxel_size,
|
||||
);
|
||||
|
||||
// Convert the normalized neighbor coordinate back to world space
|
||||
let half_size = self.size * 0.5;
|
||||
let neighbor_world = neighbor * self.size - Vec3::splat(half_size);
|
||||
|
||||
if !self.contains(neighbor_world.x, neighbor_world.y, neighbor_world.z) {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.get_voxel_at_world_coords(neighbor_world).is_some()
|
||||
}
|
||||
|
||||
|
||||
/// Performs a raycast against the octree and returns the first intersected voxel.
|
||||
pub fn raycast(&self, ray: &Ray) -> Option<(f32, f32, f32, u32, Vec3)> {
|
||||
// Start from the root node
|
||||
let half_size = self.size / 2.0;
|
||||
let root_bounds = AABB {
|
||||
min: Vec3::new(-half_size as f32, -half_size as f32, -half_size as f32),
|
||||
max: Vec3::new(half_size as f32, half_size as f32, half_size as f32),
|
||||
};
|
||||
self.raycast_recursive(
|
||||
&self.root,
|
||||
ray,
|
||||
&root_bounds,
|
||||
0,
|
||||
)
|
||||
}
|
||||
|
||||
fn raycast_recursive(
|
||||
&self,
|
||||
node: &OctreeNode,
|
||||
ray: &Ray,
|
||||
bounds: &AABB,
|
||||
depth: u32,
|
||||
) -> Option<(f32, f32, f32, u32, Vec3)> {
|
||||
// Check if the ray intersects this node's bounding box
|
||||
if let Some((t_enter, _, normal)) = self.ray_intersects_aabb_with_normal(ray, bounds) {
|
||||
// If this is a leaf node and contains a voxel, return it
|
||||
if node.is_leaf && node.voxel.is_some() {
|
||||
// Compute the exact hit position
|
||||
let hit_position = ray.origin + ray.direction * t_enter;
|
||||
|
||||
// Return the hit position along with depth and normal
|
||||
return Some((
|
||||
hit_position.x as f32,
|
||||
hit_position.y as f32,
|
||||
hit_position.z as f32,
|
||||
depth,
|
||||
normal,
|
||||
));
|
||||
}
|
||||
|
||||
// If the node has children, traverse them
|
||||
if let Some(ref children) = node.children {
|
||||
// For each child, compute its bounding box and recurse
|
||||
let mut hits = Vec::new();
|
||||
for (i, child) in children.iter().enumerate() {
|
||||
let child_bounds = self.compute_child_bounds(bounds, i);
|
||||
if let Some(hit) = self.raycast_recursive(child, ray, &child_bounds, depth + 1) {
|
||||
hits.push(hit);
|
||||
}
|
||||
}
|
||||
// Return the closest hit, if any
|
||||
if !hits.is_empty() {
|
||||
hits.sort_by(|a, b| {
|
||||
let dist_a = ((a.0 as f32 - ray.origin.x).powi(2)
|
||||
+ (a.1 as f32 - ray.origin.y).powi(2)
|
||||
+ (a.2 as f32 - ray.origin.z).powi(2))
|
||||
.sqrt();
|
||||
let dist_b = ((b.0 as f32 - ray.origin.x).powi(2)
|
||||
+ (b.1 as f32 - ray.origin.y).powi(2)
|
||||
+ (b.2 as f32 - ray.origin.z).powi(2))
|
||||
.sqrt();
|
||||
dist_a.partial_cmp(&dist_b).unwrap()
|
||||
});
|
||||
return Some(hits[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
202
client/src/plugins/environment/systems/voxels/rendering.rs
Normal file
@ -0,0 +1,202 @@
|
||||
use bevy::asset::RenderAssetUsages;
|
||||
use bevy::prelude::*;
|
||||
use bevy::render::mesh::*;
|
||||
use bevy::render::render_resource::*;
|
||||
use big_space::prelude::GridCell;
|
||||
use crate::plugins::big_space::big_space_plugin::RootGrid;
|
||||
use crate::plugins::environment::systems::voxels::structure::*;
|
||||
#[derive(Component)]
|
||||
pub struct VoxelTerrainMarker {}
|
||||
|
||||
|
||||
pub fn render(
|
||||
mut commands: Commands,
|
||||
mut query: Query<&mut SparseVoxelOctree>,
|
||||
render_object_query: Query<Entity, With<VoxelTerrainMarker>>,
|
||||
mut meshes: ResMut<Assets<Mesh>>,
|
||||
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||
root: Res<RootGrid>,
|
||||
) {
|
||||
|
||||
|
||||
for mut octree in query.iter_mut() {
|
||||
// Only update when marked dirty
|
||||
if !octree.dirty.is_empty() {
|
||||
// Remove old render objects
|
||||
for entity in render_object_query.iter() {
|
||||
info!("Despawning {}", entity);
|
||||
commands.entity(entity).despawn_recursive();
|
||||
}
|
||||
|
||||
// Get the voxel centers (world positions), color, and depth.
|
||||
let voxels = octree.traverse();
|
||||
|
||||
// Debug: Log the number of voxels traversed.
|
||||
info!("Voxel count: {}", voxels.len());
|
||||
|
||||
let mut voxel_meshes = Vec::new();
|
||||
|
||||
for (world_position, _color, depth) in voxels {
|
||||
// Get the size of the voxel at the current depth.
|
||||
let voxel_size = octree.get_spacing_at_depth(depth);
|
||||
|
||||
// The traverse method already returns the voxel center in world space.
|
||||
|
||||
// For each neighbor direction, check if this voxel face is exposed.
|
||||
for &(dx, dy, dz) in NEIGHBOR_OFFSETS.iter() {
|
||||
// Pass the world-space voxel center directly.
|
||||
if !octree.has_neighbor(world_position, dx as i32, dy as i32, dz as i32, depth) {
|
||||
|
||||
// Determine face normal and the local offset for the face.
|
||||
let (normal, offset) = match (dx, dy, dz) {
|
||||
(-1.0, 0.0, 0.0) => (
|
||||
Vec3::new(-1.0, 0.0, 0.0),
|
||||
Vec3::new(-voxel_size / 2.0, 0.0, 0.0),
|
||||
),
|
||||
(1.0, 0.0, 0.0) => (
|
||||
Vec3::new(1.0, 0.0, 0.0),
|
||||
Vec3::new(voxel_size / 2.0, 0.0, 0.0),
|
||||
),
|
||||
(0.0, -1.0, 0.0) => (
|
||||
Vec3::new(0.0, -1.0, 0.0),
|
||||
Vec3::new(0.0, -voxel_size / 2.0, 0.0),
|
||||
),
|
||||
(0.0, 1.0, 0.0) => (
|
||||
Vec3::new(0.0, 1.0, 0.0),
|
||||
Vec3::new(0.0, voxel_size / 2.0, 0.0),
|
||||
),
|
||||
(0.0, 0.0, -1.0) => (
|
||||
Vec3::new(0.0, 0.0, -1.0),
|
||||
Vec3::new(0.0, 0.0, -voxel_size / 2.0),
|
||||
),
|
||||
(0.0, 0.0, 1.0) => (
|
||||
Vec3::new(0.0, 0.0, 1.0),
|
||||
Vec3::new(0.0, 0.0, voxel_size / 2.0),
|
||||
),
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
voxel_meshes.push(generate_face(
|
||||
world_position + offset, // offset the face
|
||||
voxel_size / 2.0,
|
||||
normal
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Merge all the face meshes into a single mesh.
|
||||
let mesh = merge_meshes(voxel_meshes);
|
||||
let cube_handle = meshes.add(mesh);
|
||||
|
||||
// Create a material with cull_mode disabled to see both sides (for debugging)
|
||||
let material = materials.add(StandardMaterial {
|
||||
base_color: Color::srgba(0.8, 0.7, 0.6, 1.0),
|
||||
cull_mode: Some(Face::Back), // disable culling for debugging
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
commands.entity(root.0).with_children(|parent| {
|
||||
parent.spawn((
|
||||
PbrBundle {
|
||||
mesh: Mesh3d::from(cube_handle),
|
||||
material: MeshMaterial3d::from(material),
|
||||
transform: Transform::from_translation(Vec3::new(0.0, 0.0, 0.0)),
|
||||
..Default::default()
|
||||
},
|
||||
GridCell::<i64>::ZERO,
|
||||
VoxelTerrainMarker {},
|
||||
));
|
||||
});
|
||||
|
||||
// Reset the dirty flag after updating.
|
||||
octree.dirty.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_face(position: Vec3, face_size: f32, normal: Vec3) -> Mesh {
|
||||
// Initialize an empty mesh with triangle topology
|
||||
let mut mesh = Mesh::new(PrimitiveTopology::TriangleList, RenderAssetUsages::default());
|
||||
|
||||
// Define a quad centered at the origin
|
||||
let mut positions = vec![
|
||||
[-face_size, -face_size, 0.0],
|
||||
[ face_size, -face_size, 0.0],
|
||||
[ face_size, face_size, 0.0],
|
||||
[-face_size, face_size, 0.0],
|
||||
];
|
||||
|
||||
// Normalize the provided normal to ensure correct rotation
|
||||
let normal = normal.normalize();
|
||||
// Compute a rotation that aligns the default +Z with the provided normal
|
||||
let rotation = Quat::from_rotation_arc(Vec3::Z, normal);
|
||||
|
||||
// Rotate and translate the vertices based on the computed rotation and provided position
|
||||
for p in positions.iter_mut() {
|
||||
let vertex = rotation * Vec3::from(*p) + position;
|
||||
*p = [vertex.x, vertex.y, vertex.z];
|
||||
}
|
||||
|
||||
let uvs = vec![
|
||||
[0.0, 1.0],
|
||||
[1.0, 1.0],
|
||||
[1.0, 0.0],
|
||||
[0.0, 0.0],
|
||||
];
|
||||
|
||||
let indices = Indices::U32(vec![0, 1, 2, 2, 3, 0]);
|
||||
|
||||
// Use the provided normal for all vertices
|
||||
let normals = vec![[normal.x, normal.y, normal.z]; 4];
|
||||
|
||||
mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions);
|
||||
mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, normals);
|
||||
mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, uvs);
|
||||
mesh.insert_indices(indices);
|
||||
|
||||
mesh
|
||||
}
|
||||
|
||||
fn merge_meshes(meshes: Vec<Mesh>) -> Mesh {
|
||||
let mut merged_positions = Vec::new();
|
||||
let mut merged_uvs = Vec::new();
|
||||
let mut merged_normals = Vec::new(); // To store merged normals
|
||||
let mut merged_indices = Vec::new();
|
||||
|
||||
for mesh in meshes {
|
||||
if let Some(VertexAttributeValues::Float32x3(positions)) = mesh.attribute(Mesh::ATTRIBUTE_POSITION) {
|
||||
let start_index = merged_positions.len();
|
||||
merged_positions.extend_from_slice(positions);
|
||||
|
||||
// Extract UVs
|
||||
if let Some(VertexAttributeValues::Float32x2(uvs)) = mesh.attribute(Mesh::ATTRIBUTE_UV_0) {
|
||||
merged_uvs.extend_from_slice(uvs);
|
||||
}
|
||||
|
||||
// Extract normals
|
||||
if let Some(VertexAttributeValues::Float32x3(normals)) = mesh.attribute(Mesh::ATTRIBUTE_NORMAL) {
|
||||
merged_normals.extend_from_slice(normals);
|
||||
}
|
||||
|
||||
// Extract indices and apply offset
|
||||
if let Some(indices) = mesh.indices() {
|
||||
if let Indices::U32(indices) = indices {
|
||||
let offset_indices: Vec<u32> = indices.iter().map(|i| i + start_index as u32).collect();
|
||||
merged_indices.extend(offset_indices);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create new merged mesh
|
||||
let mut merged_mesh = Mesh::new(PrimitiveTopology::TriangleList, RenderAssetUsages::default());
|
||||
|
||||
// Insert attributes into the merged mesh
|
||||
merged_mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, merged_positions);
|
||||
merged_mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, merged_uvs);
|
||||
merged_mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, merged_normals); // Insert merged normals
|
||||
merged_mesh.insert_indices(Indices::U32(merged_indices));
|
||||
|
||||
merged_mesh
|
||||
}
|
||||
85
client/src/plugins/environment/systems/voxels/structure.rs
Normal file
@ -0,0 +1,85 @@
|
||||
use std::collections::{HashMap, HashSet, VecDeque};
|
||||
use bevy::color::Color;
|
||||
use bevy::prelude::*;
|
||||
|
||||
|
||||
/// Represents a single voxel with a color.
|
||||
#[derive(Debug, Clone, Copy, Component, PartialEq, Default)]
|
||||
pub struct Voxel {
|
||||
pub color: Color,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct DirtyVoxel {
|
||||
pub position: Vec3,
|
||||
}
|
||||
|
||||
/// Represents a node in the sparse voxel octree.
|
||||
|
||||
#[derive(Debug, Component, Clone)]
|
||||
pub struct OctreeNode {
|
||||
pub children: Option<Box<[OctreeNode; 8]>>,
|
||||
pub voxel: Option<Voxel>,
|
||||
pub is_leaf: bool,
|
||||
}
|
||||
/// Represents the root of the sparse voxel octree.
|
||||
/// Represents the root of the sparse voxel octree.
|
||||
#[derive(Debug, Component)]
|
||||
pub struct SparseVoxelOctree {
|
||||
|
||||
pub root: OctreeNode,
|
||||
pub max_depth: u32,
|
||||
pub size: f32,
|
||||
pub show_wireframe: bool,
|
||||
pub show_world_grid: bool,
|
||||
pub show_chunks: bool,
|
||||
|
||||
pub dirty: Vec<DirtyVoxel>,
|
||||
}
|
||||
|
||||
impl OctreeNode {
|
||||
/// Creates a new empty octree node.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
children: None,
|
||||
voxel: None,
|
||||
is_leaf: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.voxel.is_none() && self.children.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
impl Voxel {
|
||||
/// Creates a new empty octree node.
|
||||
pub fn new(color: Color) -> Self {
|
||||
Self {
|
||||
color,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub const NEIGHBOR_OFFSETS: [(f32, f32, f32); 6] = [
|
||||
(-1.0, 0.0, 0.0), // Left
|
||||
(1.0, 0.0, 0.0), // Right
|
||||
(0.0, -1.0, 0.0), // Down
|
||||
(0.0, 1.0, 0.0), // Up
|
||||
(0.0, 0.0, -1.0), // Back
|
||||
(0.0, 0.0, 1.0), // Front
|
||||
];
|
||||
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Ray {
|
||||
pub origin: Vec3,
|
||||
pub direction: Vec3,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AABB {
|
||||
pub min: Vec3,
|
||||
pub max: Vec3,
|
||||
}
|
||||
4
trim.bat
@ -3,8 +3,8 @@ rem combine_all.bat – merge every *.rs and *.toml in this tree
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
rem Output files
|
||||
set "OUT_RS=target/combined.rs"
|
||||
set "OUT_TOML=target/combined.toml"
|
||||
set "OUT_RS=target/combined.rs.out"
|
||||
set "OUT_TOML=target/combined.toml.out"
|
||||
|
||||
if exist "%OUT_RS%" del "%OUT_RS%"
|
||||
if exist "%OUT_TOML%" del "%OUT_TOML%"
|
||||
|
||||