Skip to content

Commit 8e363c9

Browse files
committed
feat: add raycasting
Closes #563
1 parent 846808b commit 8e363c9

File tree

9 files changed

+300
-8
lines changed

9 files changed

+300
-8
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/geometry/src/aabb.rs

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::fmt::Display;
1+
use std::{fmt::Display, ops::Add};
22

33
use glam::Vec3;
44
use serde::{Deserialize, Serialize};
@@ -32,6 +32,17 @@ impl Display for Aabb {
3232
}
3333
}
3434

35+
impl Add<Vec3> for Aabb {
36+
type Output = Self;
37+
38+
fn add(self, rhs: Vec3) -> Self::Output {
39+
Self {
40+
min: self.min + rhs,
41+
max: self.max + rhs,
42+
}
43+
}
44+
}
45+
3546
#[derive(Copy, Clone, Eq, PartialEq, Debug, Ord, PartialOrd, Hash)]
3647
pub struct CheckableAabb {
3748
pub min: [ordered_float::NotNan<f32>; 3],
@@ -200,7 +211,7 @@ impl Aabb {
200211
///
201212
/// Returns Some(t) with the distance to intersection if hit, None if no hit
202213
#[must_use]
203-
pub fn intersect_ray(&self, ray: &Ray) -> Option<f32> {
214+
pub fn intersect_ray(&self, ray: &Ray) -> Option<ordered_float::NotNan<f32>> {
204215
// Calculate t0 and t1 for all three axes simultaneously using SIMD
205216
let t0 = (self.min - ray.origin()) * ray.inv_direction();
206217
let t1 = (self.max - ray.origin()) * ray.inv_direction();
@@ -217,7 +228,11 @@ impl Aabb {
217228
return None;
218229
}
219230

220-
Some(if t_min > 0.0 { t_min } else { t_max })
231+
Some(if t_min > 0.0 {
232+
ordered_float::NotNan::new(t_min).unwrap()
233+
} else {
234+
ordered_float::NotNan::new(t_max).unwrap()
235+
})
221236
}
222237

223238
#[must_use]

crates/geometry/src/ray.rs

Lines changed: 130 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use glam::Vec3;
1+
use glam::{IVec3, Vec3};
22

33
#[derive(Debug, Clone, Copy)]
44
pub struct Ray {
@@ -41,4 +41,133 @@ impl Ray {
4141
pub fn at(&self, t: f32) -> Vec3 {
4242
self.origin + self.direction * t
4343
}
44+
45+
/// Efficiently traverse through grid cells that the ray intersects.
46+
/// Returns an iterator over the grid cells ([`IVec3`]) that the ray passes through.
47+
pub fn voxel_traversal(&self, bounds_min: IVec3, bounds_max: IVec3) -> VoxelTraversal {
48+
// Convert ray origin to grid coordinates
49+
let current_pos = self.origin.floor().as_ivec3();
50+
51+
// Calculate step direction for each axis
52+
let step = IVec3::new(
53+
if self.direction.x > 0.0 {
54+
1
55+
} else if self.direction.x < 0.0 {
56+
-1
57+
} else {
58+
0
59+
},
60+
if self.direction.y > 0.0 {
61+
1
62+
} else if self.direction.y < 0.0 {
63+
-1
64+
} else {
65+
0
66+
},
67+
if self.direction.z > 0.0 {
68+
1
69+
} else if self.direction.z < 0.0 {
70+
-1
71+
} else {
72+
0
73+
},
74+
);
75+
76+
// Calculate initial t_max values (distance to next voxel boundary for each axis)
77+
let next_voxel = current_pos + step;
78+
let t_max = Vec3::new(
79+
if self.direction.x == 0.0 {
80+
f32::INFINITY
81+
} else {
82+
((next_voxel.x as f32) - self.origin.x) * self.inv_direction.x
83+
},
84+
if self.direction.y == 0.0 {
85+
f32::INFINITY
86+
} else {
87+
((next_voxel.y as f32) - self.origin.y) * self.inv_direction.y
88+
},
89+
if self.direction.z == 0.0 {
90+
f32::INFINITY
91+
} else {
92+
((next_voxel.z as f32) - self.origin.z) * self.inv_direction.z
93+
},
94+
);
95+
96+
// Calculate t_delta values (distance between voxel boundaries)
97+
let t_delta = Vec3::new(
98+
if self.direction.x == 0.0 {
99+
f32::INFINITY
100+
} else {
101+
step.x as f32 * self.inv_direction.x
102+
},
103+
if self.direction.y == 0.0 {
104+
f32::INFINITY
105+
} else {
106+
step.y as f32 * self.inv_direction.y
107+
},
108+
if self.direction.z == 0.0 {
109+
f32::INFINITY
110+
} else {
111+
step.z as f32 * self.inv_direction.z
112+
},
113+
);
114+
115+
VoxelTraversal {
116+
current_pos,
117+
step,
118+
t_max,
119+
t_delta,
120+
bounds_min,
121+
bounds_max,
122+
}
123+
}
124+
}
125+
126+
#[derive(Debug)]
127+
#[must_use]
128+
pub struct VoxelTraversal {
129+
current_pos: IVec3,
130+
step: IVec3,
131+
t_max: Vec3,
132+
t_delta: Vec3,
133+
bounds_min: IVec3,
134+
bounds_max: IVec3,
135+
}
136+
137+
impl Iterator for VoxelTraversal {
138+
type Item = IVec3;
139+
140+
fn next(&mut self) -> Option<Self::Item> {
141+
// Check if current position is within bounds
142+
if self.current_pos.x < self.bounds_min.x
143+
|| self.current_pos.x > self.bounds_max.x
144+
|| self.current_pos.y < self.bounds_min.y
145+
|| self.current_pos.y > self.bounds_max.y
146+
|| self.current_pos.z < self.bounds_min.z
147+
|| self.current_pos.z > self.bounds_max.z
148+
{
149+
return None;
150+
}
151+
152+
let current = self.current_pos;
153+
154+
// Determine which axis to step along (the one with minimum t_max)
155+
if self.t_max.x < self.t_max.y {
156+
if self.t_max.x < self.t_max.z {
157+
self.current_pos.x += self.step.x;
158+
self.t_max.x += self.t_delta.x;
159+
} else {
160+
self.current_pos.z += self.step.z;
161+
self.t_max.z += self.t_delta.z;
162+
}
163+
} else if self.t_max.y < self.t_max.z {
164+
self.current_pos.y += self.step.y;
165+
self.t_max.y += self.t_delta.y;
166+
} else {
167+
self.current_pos.z += self.step.z;
168+
self.t_max.z += self.t_delta.z;
169+
}
170+
171+
Some(current)
172+
}
44173
}

crates/hyperion-clap/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ publish = false
1111
[dependencies]
1212
clap ={ workspace = true }
1313
flecs_ecs = { workspace = true }
14+
hyperion = { workspace = true }
1415
hyperion-command = { workspace = true }
1516
tracing = { workspace = true }
16-
hyperion = { workspace = true }
1717
valence_protocol = { workspace = true }
1818

1919
[lints]

crates/hyperion/src/simulation/blocks/mod.rs

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ use flecs_ecs::{
99
core::{Entity, World, WorldGet},
1010
macros::Component,
1111
};
12-
use glam::{IVec2, IVec3};
12+
use geometry::ray::Ray;
13+
use glam::{IVec2, IVec3, Vec3};
1314
use indexmap::IndexMap;
1415
use loader::{ChunkLoaderHandle, launch_manager};
1516
use rayon::iter::ParallelIterator;
@@ -51,6 +52,13 @@ pub enum TrySetBlockDeltaError {
5152
ChunkNotLoaded,
5253
}
5354

55+
#[derive(Debug)]
56+
pub struct RayCollision {
57+
pub distance: f32,
58+
pub location: IVec3,
59+
pub block: BlockState,
60+
}
61+
5462
/// Accessor of blocks.
5563
#[derive(Component)]
5664
pub struct Blocks {
@@ -87,6 +95,66 @@ impl Blocks {
8795
})
8896
}
8997

98+
#[must_use]
99+
pub fn first_collision(&self, ray: Ray, distance_limit: f32) -> Option<RayCollision> {
100+
let a = ray.origin();
101+
let b = ray.origin() + ray.direction() * distance_limit;
102+
103+
let min = a.min(b);
104+
let max = a.max(b);
105+
106+
let min = min.floor().as_ivec3();
107+
let max = max.ceil().as_ivec3();
108+
109+
let traversal = ray.voxel_traversal(min, max);
110+
println!("traversal = {traversal:?}");
111+
112+
let mut min: Option<RayCollision> = None;
113+
114+
for cell in traversal {
115+
println!("cell = {cell:?}");
116+
117+
// if there is no block at this cell, return None
118+
let block = self.get_block(cell)?;
119+
120+
let origin = Vec3::new(cell.x as f32, cell.y as f32, cell.z as f32);
121+
122+
let min_dist = block
123+
.collision_shapes()
124+
.map(|shape| {
125+
geometry::aabb::Aabb::new(shape.min().as_vec3(), shape.max().as_vec3())
126+
})
127+
.map(|shape| shape + origin)
128+
.filter_map(|shape| shape.intersect_ray(&ray))
129+
.min();
130+
131+
let Some(min_dist) = min_dist else {
132+
continue;
133+
};
134+
135+
match &min {
136+
Some(current_min) => {
137+
if min_dist.into_inner() < current_min.distance {
138+
min = Some(RayCollision {
139+
distance: min_dist.into_inner(),
140+
location: cell,
141+
block,
142+
});
143+
}
144+
}
145+
None => {
146+
min = Some(RayCollision {
147+
distance: min_dist.into_inner(),
148+
location: cell,
149+
block,
150+
});
151+
}
152+
}
153+
}
154+
155+
min
156+
}
157+
90158
#[must_use]
91159
pub fn par_scan_for(&self, block: BlockState) -> impl ParallelIterator<Item = IVec3> + '_ {
92160
use rayon::prelude::*;

crates/hyperion/src/simulation/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,9 @@ pub struct Name(Arc<str>);
128128
#[derive(Component, Deref, DerefMut, From, Debug, Default)]
129129
pub struct IgnMap(DeferredMap<Arc<str>, Entity>);
130130

131+
#[derive(Component, Debug, Default)]
132+
pub struct RaycastTravel;
133+
131134
/// A component that represents a Player. In the future, this should be broken up into multiple components.
132135
///
133136
/// Why should it be broken up? The more things are broken up, the more we can take advantage of Rust borrowing rules.

events/tag/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ tracing = { workspace = true }
2323
rayon = { workspace = true }
2424
gxhash = { workspace = true }
2525
derive_more = { workspace = true }
26+
geometry = { workspace = true }
2627

2728
[dev-dependencies]
2829
tracing = {workspace = true, features = ["release_max_level_info"]}

events/tag/src/command.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ use flecs_ecs::core::World;
22
use hyperion_clap::{MinecraftCommand, hyperion_command::CommandRegistry};
33

44
use crate::command::{
5-
fly::FlyCommand, rank::ClassCommand, replace::ReplaceCommand, speed::SpeedCommand,
6-
xp::XpCommand,
5+
dirt::DirtCommand, fly::FlyCommand, rank::ClassCommand, replace::ReplaceCommand,
6+
speed::SpeedCommand, xp::XpCommand,
77
};
88

9+
mod dirt;
910
mod fly;
1011
mod rank;
1112
mod replace;
@@ -18,4 +19,5 @@ pub fn register(registry: &mut CommandRegistry, world: &World) {
1819
ClassCommand::register(registry, world);
1920
XpCommand::register(registry, world);
2021
ReplaceCommand::register(registry, world);
22+
DirtCommand::register(registry, world);
2123
}

0 commit comments

Comments
 (0)