Skip to content

Commit

Permalink
Merge pull request splashdust#25 from dtaralla/raycast_fn_using_fast_…
Browse files Browse the repository at this point in the history
…traversal

Raycast fn using fast traversal
  • Loading branch information
splashdust authored Jun 5, 2024
2 parents 1a925f5 + eede4c6 commit 866c837
Show file tree
Hide file tree
Showing 7 changed files with 145 additions and 95 deletions.
17 changes: 10 additions & 7 deletions examples/fast_traversal_ray.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,13 +151,16 @@ fn update_cursor_cube(
if let Some(result) = voxel_world_raycast.raycast(ray, &|(_pos, _vox)| true) {
let (mut transform, mut cursor_cube) = cursor_cube.single_mut();

// Move the cursor cube to the position of the voxel we hit
let voxel_pos = result.position + result.normal;
transform.translation = voxel_pos + Vec3::splat(VOXEL_SIZE / 2.);
cursor_cube.voxel_pos = voxel_pos.as_ivec3();

// Update current trace end to the cursor cube position
trace.end = transform.translation;
// Camera could end up inside geometry - in that case just ignore the trace
if let Some(normal) = result.normal {
// Move the cursor cube to the position of the voxel we hit
let voxel_pos = result.position + normal;
transform.translation = voxel_pos + Vec3::splat(VOXEL_SIZE / 2.);
cursor_cube.voxel_pos = voxel_pos.as_ivec3();

// Update current trace end to the cursor cube position
trace.end = transform.translation;
}
}
}
}
Expand Down
7 changes: 5 additions & 2 deletions examples/ray_cast.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use std::sync::Arc;

use bevy::prelude::*;

use bevy_voxel_world::prelude::*;
use std::sync::Arc;

// Declare materials as consts for convenience
const SNOWY_BRICK: u8 = 0;
Expand Down Expand Up @@ -120,7 +122,8 @@ fn update_cursor_cube(
if let Some(result) = voxel_world_raycast.raycast(ray, &|(_pos, _vox)| true) {
let (mut transform, mut cursor_cube) = cursor_cube.single_mut();
// Move the cursor cube to the position of the voxel we hit
let voxel_pos = result.position + result.normal;
// Camera is by construction not in a solid voxel, so result.normal must be Some(...)
let voxel_pos = result.position + result.normal.unwrap();
transform.translation = voxel_pos + Vec3::new(0.5, 0.5, 0.5);
cursor_cube.voxel_pos = voxel_pos.as_ivec3();
}
Expand Down
84 changes: 68 additions & 16 deletions src/chunk_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,42 +3,64 @@ use std::{
sync::{Arc, RwLock, RwLockReadGuard},
};

use bevy::{prelude::*, utils::hashbrown::HashMap};
use bevy::{math::bounding::Aabb3d, prelude::*, utils::hashbrown::HashMap};

use crate::{
chunk::{self, ChunkData},
chunk::{self, ChunkData, CHUNK_SIZE_F},
voxel::VOXEL_SIZE,
voxel_world::ChunkWillSpawn,
};

#[derive(Deref, DerefMut)]
pub struct ChunkMapData {
#[deref]
data: HashMap<IVec3, chunk::ChunkData>,
bounds: Aabb3d,
}

/// Holds a map of all chunks that are currently spawned spawned
/// The chunks also exist as entities that can be queried in the ECS,
/// but having this map in addition allows for faster spatial lookups
#[derive(Resource)]
pub struct ChunkMap<C> {
map: Arc<RwLock<HashMap<IVec3, chunk::ChunkData>>>,
map: Arc<RwLock<ChunkMapData>>,
_marker: PhantomData<C>,
}

impl<C: Send + Sync + 'static> ChunkMap<C> {
pub fn get(
position: &IVec3,
read_lock: &RwLockReadGuard<HashMap<IVec3, chunk::ChunkData>>,
read_lock: &RwLockReadGuard<ChunkMapData>,
) -> Option<chunk::ChunkData> {
read_lock.get(position).cloned()
read_lock.data.get(position).cloned()
}

pub fn contains_chunk(
position: &IVec3,
read_lock: &RwLockReadGuard<HashMap<IVec3, chunk::ChunkData>>,
) -> bool {
read_lock.contains_key(position)
pub fn contains_chunk(position: &IVec3, read_lock: &RwLockReadGuard<ChunkMapData>) -> bool {
read_lock.data.contains_key(position)
}

/// Get the current bounding box of loaded chunks in this map.
///
/// Expressed in **chunk coordinates**. Bounds are **inclusive**.
pub fn get_bounds(read_lock: &RwLockReadGuard<ChunkMapData>) -> Aabb3d {
read_lock.bounds
}

/// Get the current bounding box of loaded chunks in this map.
///
/// Expressed in **world units**. Bounds are **inclusive**.
pub fn get_world_bounds(read_lock: &RwLockReadGuard<ChunkMapData>) -> Aabb3d {
let mut world_bounds = ChunkMap::<C>::get_bounds(read_lock);
world_bounds.min *= CHUNK_SIZE_F * VOXEL_SIZE;
world_bounds.max = (world_bounds.max + Vec3::ONE) * CHUNK_SIZE_F * VOXEL_SIZE;
world_bounds
}

pub fn get_read_lock(&self) -> RwLockReadGuard<HashMap<IVec3, chunk::ChunkData>> {
pub fn get_read_lock(&self) -> RwLockReadGuard<ChunkMapData> {
self.map.read().unwrap()
}

pub fn get_map(&self) -> Arc<RwLock<HashMap<IVec3, chunk::ChunkData>>> {
pub fn get_map(&self) -> Arc<RwLock<ChunkMapData>> {
self.map.clone()
}

Expand All @@ -55,40 +77,70 @@ impl<C: Send + Sync + 'static> ChunkMap<C> {

if let Ok(mut write_lock) = self.map.try_write() {
for (position, chunk_data) in insert_buffer.iter() {
write_lock.insert(
write_lock.data.insert(
*position,
ChunkData {
position: *position,
..chunk_data.clone()
},
);

let position_f = position.as_vec3();
if position_f.cmplt(write_lock.bounds.min).any() {
write_lock.bounds.min = position_f.min(write_lock.bounds.min);
} else if position_f.cmpgt(write_lock.bounds.max).any() {
write_lock.bounds.max = position_f.max(write_lock.bounds.max);
}
}
insert_buffer.clear();

for (position, chunk_data, evt) in update_buffer.iter() {
write_lock.insert(
write_lock.data.insert(
*position,
ChunkData {
position: *position,
..chunk_data.clone()
},
);

let position_f = position.as_vec3();
if position_f.cmplt(write_lock.bounds.min).any() {
write_lock.bounds.min = position_f.min(write_lock.bounds.min);
} else if position_f.cmpgt(write_lock.bounds.max).any() {
write_lock.bounds.max = position_f.max(write_lock.bounds.max);
}

ev_chunk_will_spawn.send((*evt).clone());
}
update_buffer.clear();

let mut need_rebuild_aabb = false;
for position in remove_buffer.iter() {
write_lock.remove(position);
write_lock.data.remove(position);

need_rebuild_aabb = write_lock.bounds.min.floor().as_ivec3() == *position
|| write_lock.bounds.max.floor().as_ivec3() == *position;
}
remove_buffer.clear();

if need_rebuild_aabb {
let mut tmp_vec = Vec::with_capacity(write_lock.data.len());
for v in write_lock.data.keys() {
tmp_vec.push(v.as_vec3());
}
write_lock.bounds = Aabb3d::from_point_cloud(Vec3::ZERO, Quat::IDENTITY, &tmp_vec);
}
}
}
}

impl<C> Default for ChunkMap<C> {
fn default() -> Self {
Self {
map: Arc::new(RwLock::new(HashMap::with_capacity(1000))),
map: Arc::new(RwLock::new(ChunkMapData {
data: HashMap::with_capacity(1000),
bounds: Aabb3d::new(Vec3::ZERO, Vec3::ZERO),
})),
_marker: PhantomData,
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -233,8 +233,8 @@ fn raycast_finds_voxel() {
result,
VoxelRaycastResult {
position: Vec3::ZERO,
normal: Vec3::new(0.0, 0.0, 1.0),
voxel: test_voxel
normal: Some(Vec3::new(0.0, 0.0, 1.0)),
voxel: test_voxel,
}
)
});
Expand Down
1 change: 1 addition & 0 deletions src/voxel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ impl TryFrom<VoxelFace> for Vec3 {
}
}

#[allow(unused)]
pub(crate) trait VoxelAabb {
fn ray_intersection(&self, ray: Ray3d) -> Option<(Vec3, Vec3)>;
}
Expand Down
122 changes: 56 additions & 66 deletions src/voxel_world.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@
/// VoxelWorld
/// This module implements most of the public API for bevy_voxel_world.
///
use bevy::{ecs::system::SystemParam, prelude::*, render::primitives::Aabb};
use std::marker::PhantomData;
use std::sync::Arc;

use bevy::{ecs::system::SystemParam, math::bounding::RayCast3d, prelude::*};

use crate::{
chunk::{CHUNK_SIZE_F, CHUNK_SIZE_I},
chunk_map::ChunkMap,
configuration::VoxelWorldConfig,
voxel::{VoxelAabb, WorldVoxel},
traversal_alg::voxel_line_traversal,
voxel::WorldVoxel,
voxel_world_internal::{get_chunk_voxel_position, ModifiedVoxels, VoxelWriteBuffer},
};

Expand Down Expand Up @@ -78,7 +79,7 @@ pub type RaycastFn = dyn Fn(Ray3d, &dyn FilterFn) -> Option<VoxelRaycastResult>
#[derive(Default, Debug, PartialEq, Clone)]
pub struct VoxelRaycastResult {
pub position: Vec3,
pub normal: Vec3,
pub normal: Option<Vec3>,
pub voxel: WorldVoxel,
}

Expand All @@ -89,19 +90,18 @@ impl VoxelRaycastResult {
}

/// Get the face normal of the ray hit
pub fn voxel_normal(&self) -> IVec3 {
self.normal.floor().as_ivec3()
pub fn voxel_normal(&self) -> Option<IVec3> {
self.normal.map(|n| n.floor().as_ivec3())
}
}

const STEP_SIZE: f32 = 0.01;

/// Grants access to the VoxelWorld in systems
#[derive(SystemParam)]
pub struct VoxelWorld<'w, C: VoxelWorldConfig> {
chunk_map: Res<'w, ChunkMap<C>>,
modified_voxels: Res<'w, ModifiedVoxels<C>>,
voxel_write_buffer: ResMut<'w, VoxelWriteBuffer<C>>,
#[allow(unused)]
configuration: Res<'w, C>,
}

Expand Down Expand Up @@ -231,11 +231,6 @@ impl<'w, C: VoxelWorldConfig> VoxelWorld<'w, C> {
/// Returns a `VoxelRaycastResult` with position, normal and voxel info. The position is given in world space.
/// Returns `None` if no voxel was intersected
///
/// Note: The method used for raycasting here is not 100% accurate. It is possible for the ray to miss a voxel
/// if the ray is very close to the edge. This is because the raycast is done in steps of 0.01 units.
/// If you need 100% accuracy, it may be better to cast against the mesh instead, using something like `bevy_mod_raycast`
/// or some physics plugin.
///
/// # Example
/// ```
/// use bevy::prelude::*;
Expand Down Expand Up @@ -271,65 +266,60 @@ impl<'w, C: VoxelWorldConfig> VoxelWorld<'w, C> {
/// Get a sendable closure that can be used to raycast into the voxel world
pub fn raycast_fn(&self) -> Arc<RaycastFn> {
let chunk_map = self.chunk_map.get_map();
let spawning_distance = self.configuration.spawning_distance() as i32;
let get_voxel = self.get_voxel_fn();

Arc::new(move |ray, filter| {
let chunk_map_read_lock = chunk_map.read().unwrap();
let mut current = ray.origin;
let mut t = 0.0;

while t < (spawning_distance * CHUNK_SIZE_I) as f32 {
let chunk_pos = (current / CHUNK_SIZE_F).floor().as_ivec3();

if let Some(chunk_data) = ChunkMap::<C>::get(&chunk_pos, &chunk_map_read_lock) {
if !chunk_data.is_empty {
let mut voxel = WorldVoxel::Unset;
while voxel == WorldVoxel::Unset && chunk_data.encloses_point(current) {
let mut voxel_pos = current.floor().as_ivec3();
voxel = get_voxel(voxel_pos);
if voxel.is_solid() {
let mut normal = get_hit_normal(voxel_pos, ray).unwrap();

let mut adjacent_vox = get_voxel(voxel_pos + normal.as_ivec3());

// When we get here we have an approximate hit position and normal,
// so we refine until the position adjacent to the normal is empty.
let mut steps = 0;
while adjacent_vox.is_solid() && steps < 3 {
steps += 1;
voxel = adjacent_vox;
voxel_pos += normal.as_ivec3();
normal = get_hit_normal(voxel_pos, ray).unwrap_or(normal);
adjacent_vox = get_voxel(voxel_pos + normal.as_ivec3());
}

if filter.call((voxel_pos.as_vec3(), voxel)) {
return Some(VoxelRaycastResult {
position: voxel_pos.as_vec3(),
normal,
voxel,
});
}
}
t += STEP_SIZE;
current = ray.origin + ray.direction * t;
}
let p = ray.origin;
let d = *ray.direction;

let loaded_aabb = ChunkMap::<C>::get_world_bounds(&chunk_map.read().unwrap());
let trace_start = if p.cmplt(loaded_aabb.min).any() || p.cmpgt(loaded_aabb.max).any() {
if let Some(trace_start_t) =
RayCast3d::from_ray(ray, f32::MAX).aabb_intersection_at(&loaded_aabb)
{
ray.get_point(trace_start_t)
} else {
return None;
}
} else {
p
};

// To find where we get out of the loaded cuboid, we can intersect from a point
// guaranteed to be on the other side of the cube and in the opposite direction
// of the ray.
let trace_end_orig =
trace_start + d * loaded_aabb.min.distance_squared(loaded_aabb.max);
let trace_end_t = RayCast3d::new(trace_end_orig, -ray.direction, f32::MAX)
.aabb_intersection_at(&loaded_aabb)
.unwrap();
let trace_end = Ray3d::new(trace_end_orig, -d).get_point(trace_end_t);

let mut raycast_result = None;
voxel_line_traversal(trace_start, trace_end, |voxel_coords, _time, face| {
let voxel = get_voxel(voxel_coords);

if !voxel.is_unset() && filter.call((voxel_coords.as_vec3(), voxel)) {
if voxel.is_solid() {
raycast_result = Some(VoxelRaycastResult {
position: voxel_coords.as_vec3(),
normal: face.try_into().ok(),
voxel,
});

// Found solid voxel - stop traversing
false
} else {
// Voxel is not solid - continue traversing
true
}
} else {
// Ignoring this voxel bc of filter - continue traversing
true
}
});

t += STEP_SIZE;
current = ray.origin + ray.direction * t;
}
None
raycast_result
})
}
}

fn get_hit_normal(vox_pos: IVec3, ray: Ray3d) -> Option<Vec3> {
let voxel_aabb = Aabb::from_min_max(vox_pos.as_vec3(), vox_pos.as_vec3() + Vec3::ONE);

let (_, normal) = voxel_aabb.ray_intersection(ray)?;

Some(normal)
}
Loading

0 comments on commit 866c837

Please sign in to comment.