mirror of
https://github.com/italicsjenga/valence.git
synced 2025-01-11 15:21:31 +11:00
Redesign the spatial index API and add raycast example
This commit is contained in:
parent
9448e17607
commit
4a12def900
|
@ -527,7 +527,7 @@ const AGEABLE_MOB: Class = Class {
|
||||||
|
|
||||||
const ANIMAL: Class = Class {
|
const ANIMAL: Class = Class {
|
||||||
name: "animal",
|
name: "animal",
|
||||||
inherit: Some(&PATHFINDER_MOB),
|
inherit: Some(&AGEABLE_MOB),
|
||||||
fields: &[],
|
fields: &[],
|
||||||
events: &[],
|
events: &[],
|
||||||
};
|
};
|
||||||
|
|
|
@ -34,7 +34,7 @@ struct Game {
|
||||||
|
|
||||||
const MAX_PLAYERS: usize = 10;
|
const MAX_PLAYERS: usize = 10;
|
||||||
|
|
||||||
const SPAWN_POS: BlockPos = BlockPos::new(0, 99, -35);
|
const SPAWN_POS: BlockPos = BlockPos::new(0, 100, -35);
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Config for Game {
|
impl Config for Game {
|
||||||
|
@ -139,7 +139,6 @@ impl Config for Game {
|
||||||
|
|
||||||
let radius = 6.0 + ((time * TAU / 2.5).sin() + 1.0) / 2.0 * 10.0;
|
let radius = 6.0 + ((time * TAU / 2.5).sin() + 1.0) / 2.0 * 10.0;
|
||||||
|
|
||||||
// TODO: use eye position.
|
|
||||||
let player_pos = server
|
let player_pos = server
|
||||||
.clients
|
.clients
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -147,19 +146,22 @@ impl Config for Game {
|
||||||
.map(|c| c.1.position())
|
.map(|c| c.1.position())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
// TODO: hardcoded eye pos.
|
||||||
|
let eye_pos = Vec3::new(player_pos.x, player_pos.y + 1.6, player_pos.z);
|
||||||
|
|
||||||
for (cow_id, p) in cows.iter().cloned().zip(fibonacci_spiral(cow_count)) {
|
for (cow_id, p) in cows.iter().cloned().zip(fibonacci_spiral(cow_count)) {
|
||||||
let cow = server.entities.get_mut(cow_id).expect("missing cow");
|
let cow = server.entities.get_mut(cow_id).expect("missing cow");
|
||||||
let rotated = p * rot;
|
let rotated = p * rot;
|
||||||
let transformed = rotated * radius + [0.5, 100.0, 0.5];
|
let transformed = rotated * radius + [0.5, SPAWN_POS.y as f64 + 1.0, 0.5];
|
||||||
|
|
||||||
let yaw = f32::atan2(rotated.z as f32, rotated.x as f32).to_degrees() - 90.0;
|
let yaw = f32::atan2(rotated.z as f32, rotated.x as f32).to_degrees() - 90.0;
|
||||||
let (looking_yaw, looking_pitch) =
|
let (looking_yaw, looking_pitch) =
|
||||||
to_yaw_and_pitch((player_pos - transformed).normalized());
|
to_yaw_and_pitch((eye_pos - transformed).normalized());
|
||||||
|
|
||||||
cow.set_position(transformed);
|
cow.set_position(transformed);
|
||||||
cow.set_yaw(yaw);
|
cow.set_yaw(yaw);
|
||||||
cow.set_pitch(looking_pitch);
|
cow.set_pitch(looking_pitch as f32);
|
||||||
cow.set_head_yaw(looking_yaw);
|
cow.set_head_yaw(looking_yaw as f32);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
172
examples/raycast.rs
Normal file
172
examples/raycast.rs
Normal file
|
@ -0,0 +1,172 @@
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
|
|
||||||
|
use log::LevelFilter;
|
||||||
|
use valence::async_trait;
|
||||||
|
use valence::block::{BlockPos, BlockState};
|
||||||
|
use valence::client::GameMode;
|
||||||
|
use valence::config::{Config, ServerListPing};
|
||||||
|
use valence::dimension::DimensionId;
|
||||||
|
use valence::entity::{EntityData, EntityKind};
|
||||||
|
use valence::server::{Server, SharedServer, ShutdownResult};
|
||||||
|
use valence::text::{Color, TextFormat};
|
||||||
|
use valence::util::from_yaw_and_pitch;
|
||||||
|
use vek::Vec3;
|
||||||
|
|
||||||
|
pub fn main() -> ShutdownResult {
|
||||||
|
env_logger::Builder::new()
|
||||||
|
.filter_module("valence", LevelFilter::Trace)
|
||||||
|
.parse_default_env()
|
||||||
|
.init();
|
||||||
|
|
||||||
|
valence::start_server(Game {
|
||||||
|
player_count: AtomicUsize::new(0),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Game {
|
||||||
|
player_count: AtomicUsize,
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_PLAYERS: usize = 10;
|
||||||
|
|
||||||
|
const SPAWN_POS: BlockPos = BlockPos::new(0, 100, -8);
|
||||||
|
|
||||||
|
const PLAYER_EYE_HEIGHT: f64 = 1.6;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Config for Game {
|
||||||
|
fn max_connections(&self) -> usize {
|
||||||
|
// We want status pings to be successful even if the server is full.
|
||||||
|
MAX_PLAYERS + 64
|
||||||
|
}
|
||||||
|
|
||||||
|
fn online_mode(&self) -> bool {
|
||||||
|
// You'll want this to be true on real servers.
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn server_list_ping(
|
||||||
|
&self,
|
||||||
|
_server: &SharedServer,
|
||||||
|
_remote_addr: SocketAddr,
|
||||||
|
) -> ServerListPing {
|
||||||
|
ServerListPing::Respond {
|
||||||
|
online_players: self.player_count.load(Ordering::SeqCst) as i32,
|
||||||
|
max_players: MAX_PLAYERS as i32,
|
||||||
|
description: "Hello Valence!".color(Color::AQUA),
|
||||||
|
favicon_png: Some(include_bytes!("../assets/favicon.png")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init(&self, server: &mut Server) {
|
||||||
|
let (world_id, world) = server.worlds.create(DimensionId::default());
|
||||||
|
world.meta.set_flat(true);
|
||||||
|
|
||||||
|
let size = 5;
|
||||||
|
for z in -size..size {
|
||||||
|
for x in -size..size {
|
||||||
|
world.chunks.create([x, z]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
world.chunks.set_block_state(SPAWN_POS, BlockState::BEDROCK);
|
||||||
|
|
||||||
|
const SHEEP_COUNT: usize = 10;
|
||||||
|
for i in 0..SHEEP_COUNT {
|
||||||
|
let offset = (i as f64 - (SHEEP_COUNT - 1) as f64 / 2.0) * 1.25;
|
||||||
|
|
||||||
|
let (_, sheep) = server.entities.create(EntityKind::Sheep);
|
||||||
|
sheep.set_world(world_id);
|
||||||
|
sheep.set_position([offset + 0.5, SPAWN_POS.y as f64 + 1.0, 0.0]);
|
||||||
|
sheep.set_yaw(180.0);
|
||||||
|
sheep.set_head_yaw(180.0);
|
||||||
|
|
||||||
|
if let EntityData::Sheep(sheep) = sheep.data_mut() {
|
||||||
|
// Shear the sheep.
|
||||||
|
sheep.set_sheep_state(0b1_0000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&self, server: &mut Server) {
|
||||||
|
let (world_id, world) = server.worlds.iter_mut().next().unwrap();
|
||||||
|
|
||||||
|
let mut hit_entities = HashSet::new();
|
||||||
|
|
||||||
|
server.clients.retain(|_, client| {
|
||||||
|
if client.created_tick() == server.shared.current_tick() {
|
||||||
|
if self
|
||||||
|
.player_count
|
||||||
|
.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |count| {
|
||||||
|
(count < MAX_PLAYERS).then_some(count + 1)
|
||||||
|
})
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
client.disconnect("The server is full!".color(Color::RED));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
client.spawn(world_id);
|
||||||
|
client.set_game_mode(GameMode::Creative);
|
||||||
|
client.teleport(
|
||||||
|
[
|
||||||
|
SPAWN_POS.x as f64 + 0.5,
|
||||||
|
SPAWN_POS.y as f64 + 1.0,
|
||||||
|
SPAWN_POS.z as f64 + 0.5,
|
||||||
|
],
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
world.meta.player_list_mut().insert(
|
||||||
|
client.uuid(),
|
||||||
|
client.username().to_owned(),
|
||||||
|
client.textures().cloned(),
|
||||||
|
client.game_mode(),
|
||||||
|
0,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
client.send_message(
|
||||||
|
"Look at a sheep to change its ".italic()
|
||||||
|
+ "color".italic().color(Color::GREEN)
|
||||||
|
+ ".",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if client.is_disconnected() {
|
||||||
|
self.player_count.fetch_sub(1, Ordering::SeqCst);
|
||||||
|
world.meta.player_list_mut().remove(client.uuid());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let client_pos = client.position();
|
||||||
|
|
||||||
|
let origin = Vec3::new(client_pos.x, client_pos.y + PLAYER_EYE_HEIGHT, client_pos.z);
|
||||||
|
let direction = from_yaw_and_pitch(client.yaw() as f64, client.pitch() as f64);
|
||||||
|
|
||||||
|
if let Some(hit) = world.spatial_index.raycast(origin, direction, |hit| {
|
||||||
|
server
|
||||||
|
.entities
|
||||||
|
.get(hit.entity)
|
||||||
|
.map_or(false, |e| e.kind() == EntityKind::Sheep)
|
||||||
|
}) {
|
||||||
|
hit_entities.insert(hit.entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
});
|
||||||
|
|
||||||
|
for (id, e) in server.entities.iter_mut() {
|
||||||
|
if let EntityData::Sheep(sheep) = e.data_mut() {
|
||||||
|
if hit_entities.contains(&id) {
|
||||||
|
sheep.set_sheep_state(5);
|
||||||
|
} else {
|
||||||
|
sheep.set_sheep_state(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -32,10 +32,7 @@ impl Encode for BlockPos {
|
||||||
let (x, y, z) = (self.x as u64, self.y as u64, self.z as u64);
|
let (x, y, z) = (self.x as u64, self.y as u64, self.z as u64);
|
||||||
(x << 38 | z << 38 >> 26 | y & 0xfff).encode(w)
|
(x << 38 | z << 38 >> 26 | y & 0xfff).encode(w)
|
||||||
}
|
}
|
||||||
_ => bail!(
|
_ => bail!("out of range: {self:?}"),
|
||||||
"block position {:?} is out of range",
|
|
||||||
(self.x, self.y, self.z)
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
139
src/bvh.rs
139
src/bvh.rs
|
@ -3,10 +3,13 @@
|
||||||
//! [bvh]: https://en.wikipedia.org/wiki/Bounding_volume_hierarchy
|
//! [bvh]: https://en.wikipedia.org/wiki/Bounding_volume_hierarchy
|
||||||
//! [`SpatialIndex`]: crate::spatial_index::SpatialIndex
|
//! [`SpatialIndex`]: crate::spatial_index::SpatialIndex
|
||||||
|
|
||||||
|
use std::iter::FusedIterator;
|
||||||
use std::mem;
|
use std::mem;
|
||||||
|
|
||||||
use approx::relative_eq;
|
use approx::relative_eq;
|
||||||
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
|
use rayon::iter::{
|
||||||
|
IndexedParallelIterator, IntoParallelRefIterator, IntoParallelRefMutIterator, ParallelIterator,
|
||||||
|
};
|
||||||
use vek::Aabb;
|
use vek::Aabb;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
@ -16,13 +19,6 @@ pub struct Bvh<T> {
|
||||||
root: NodeIdx,
|
root: NodeIdx,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum TraverseStep<T> {
|
|
||||||
Miss,
|
|
||||||
Hit,
|
|
||||||
Return(T),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct InternalNode {
|
struct InternalNode {
|
||||||
bb: Aabb<f64>,
|
bb: Aabb<f64>,
|
||||||
|
@ -33,7 +29,7 @@ struct InternalNode {
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct LeafNode<T> {
|
struct LeafNode<T> {
|
||||||
bb: Aabb<f64>,
|
bb: Aabb<f64>,
|
||||||
id: T,
|
data: T,
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: we could use usize here to store more elements.
|
// TODO: we could use usize here to store more elements.
|
||||||
|
@ -53,7 +49,7 @@ impl<T: Send + Sync> Bvh<T> {
|
||||||
self.internal_nodes.clear();
|
self.internal_nodes.clear();
|
||||||
|
|
||||||
self.leaf_nodes
|
self.leaf_nodes
|
||||||
.extend(leaves.into_iter().map(|(id, bb)| LeafNode { bb, id }));
|
.extend(leaves.into_iter().map(|(id, bb)| LeafNode { bb, data: id }));
|
||||||
|
|
||||||
let leaf_count = self.leaf_nodes.len();
|
let leaf_count = self.leaf_nodes.len();
|
||||||
|
|
||||||
|
@ -98,40 +94,88 @@ impl<T: Send + Sync> Bvh<T> {
|
||||||
debug_assert_eq!(self.internal_nodes.len(), self.leaf_nodes.len() - 1);
|
debug_assert_eq!(self.internal_nodes.len(), self.leaf_nodes.len() - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn traverse<F, U>(&self, mut f: F) -> Option<U>
|
pub fn traverse(&self) -> Option<Node<T>> {
|
||||||
where
|
|
||||||
F: FnMut(Option<&T>, Aabb<f64>) -> TraverseStep<U>,
|
|
||||||
{
|
|
||||||
if !self.leaf_nodes.is_empty() {
|
if !self.leaf_nodes.is_empty() {
|
||||||
self.traverse_rec(self.root, &mut f)
|
Some(Node::from_idx(self, self.root))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn traverse_rec<F, U>(&self, idx: NodeIdx, f: &mut F) -> Option<U>
|
pub fn iter(
|
||||||
where
|
&self,
|
||||||
F: FnMut(Option<&T>, Aabb<f64>) -> TraverseStep<U>,
|
) -> impl ExactSizeIterator<Item = (&T, Aabb<f64>)> + FusedIterator + Clone + '_ {
|
||||||
{
|
self.leaf_nodes.iter().map(|leaf| (&leaf.data, leaf.bb))
|
||||||
if idx < self.internal_nodes.len() as NodeIdx {
|
|
||||||
let internal = &self.internal_nodes[idx as usize];
|
|
||||||
|
|
||||||
match f(None, internal.bb) {
|
|
||||||
TraverseStep::Miss => None,
|
|
||||||
TraverseStep::Hit => self
|
|
||||||
.traverse_rec(internal.left, f)
|
|
||||||
.or_else(|| self.traverse_rec(internal.right, f)),
|
|
||||||
TraverseStep::Return(u) => Some(u),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn iter_mut(
|
||||||
|
&mut self,
|
||||||
|
) -> impl ExactSizeIterator<Item = (&mut T, Aabb<f64>)> + FusedIterator + '_ {
|
||||||
|
self.leaf_nodes
|
||||||
|
.iter_mut()
|
||||||
|
.map(|leaf| (&mut leaf.data, leaf.bb))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn par_iter(&self) -> impl IndexedParallelIterator<Item = (&T, Aabb<f64>)> + Clone + '_ {
|
||||||
|
self.leaf_nodes.par_iter().map(|leaf| (&leaf.data, leaf.bb))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn par_iter_mut(
|
||||||
|
&mut self,
|
||||||
|
) -> impl IndexedParallelIterator<Item = (&mut T, Aabb<f64>)> + '_ {
|
||||||
|
self.leaf_nodes
|
||||||
|
.par_iter_mut()
|
||||||
|
.map(|leaf| (&mut leaf.data, leaf.bb))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Node<'a, T> {
|
||||||
|
Internal(Internal<'a, T>),
|
||||||
|
Leaf { data: &'a T, bb: Aabb<f64> },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T> Node<'a, T> {
|
||||||
|
fn from_idx(bvh: &'a Bvh<T>, idx: NodeIdx) -> Self {
|
||||||
|
if idx < bvh.internal_nodes.len() as NodeIdx {
|
||||||
|
Self::Internal(Internal { bvh, idx })
|
||||||
} else {
|
} else {
|
||||||
let leaf = &self.leaf_nodes[(idx - self.internal_nodes.len() as NodeIdx) as usize];
|
let leaf = &bvh.leaf_nodes[(idx - bvh.internal_nodes.len() as NodeIdx) as usize];
|
||||||
|
Self::Leaf {
|
||||||
|
data: &leaf.data,
|
||||||
|
bb: leaf.bb,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
match f(Some(&leaf.id), leaf.bb) {
|
pub fn bb(&self) -> Aabb<f64> {
|
||||||
TraverseStep::Miss | TraverseStep::Hit => None,
|
match self {
|
||||||
TraverseStep::Return(u) => Some(u),
|
Node::Internal(int) => int.bb(),
|
||||||
|
Node::Leaf { bb, .. } => *bb,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct Internal<'a, T> {
|
||||||
|
bvh: &'a Bvh<T>,
|
||||||
|
idx: NodeIdx,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T> Internal<'a, T> {
|
||||||
|
pub fn split(self) -> (Aabb<f64>, Node<'a, T>, Node<'a, T>) {
|
||||||
|
let internal = &self.bvh.internal_nodes[self.idx as usize];
|
||||||
|
|
||||||
|
let bb = internal.bb;
|
||||||
|
let left = Node::from_idx(self.bvh, internal.left);
|
||||||
|
let right = Node::from_idx(self.bvh, internal.right);
|
||||||
|
|
||||||
|
(bb, left, right)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bb(&self) -> Aabb<f64> {
|
||||||
|
self.bvh.internal_nodes[self.idx as usize].bb
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_rec<T: Send>(
|
fn build_rec<T: Send>(
|
||||||
|
@ -266,34 +310,3 @@ impl<T: Send + Sync> Default for Bvh<T> {
|
||||||
Self::new()
|
Self::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn empty() {
|
|
||||||
let mut bvh = Bvh::new();
|
|
||||||
|
|
||||||
bvh.traverse(|_, _| TraverseStep::Return(()));
|
|
||||||
bvh.build([]);
|
|
||||||
|
|
||||||
bvh.build([(5, Aabb::default())]);
|
|
||||||
bvh.traverse(|_, _| TraverseStep::Return(()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn overlapping() {
|
|
||||||
let mut bvh = Bvh::new();
|
|
||||||
|
|
||||||
bvh.build([
|
|
||||||
((), Aabb::default()),
|
|
||||||
((), Aabb::default()),
|
|
||||||
((), Aabb::default()),
|
|
||||||
((), Aabb::default()),
|
|
||||||
((), Aabb::new_empty(5.0.into())),
|
|
||||||
]);
|
|
||||||
|
|
||||||
bvh.traverse(|_, _| TraverseStep::Return(()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
//! Efficient spatial entity queries.
|
//! Efficient spatial entity queries.
|
||||||
|
|
||||||
|
use std::iter::FusedIterator;
|
||||||
|
|
||||||
|
use rayon::iter::{IndexedParallelIterator, ParallelIterator};
|
||||||
use vek::{Aabb, Vec3};
|
use vek::{Aabb, Vec3};
|
||||||
|
|
||||||
use crate::bvh::Bvh;
|
use crate::bvh::{Bvh, Node};
|
||||||
pub use crate::bvh::TraverseStep;
|
|
||||||
use crate::entity::{Entities, EntityId};
|
use crate::entity::{Entities, EntityId};
|
||||||
|
use crate::util::ray_box_intersect;
|
||||||
use crate::world::WorldId;
|
use crate::world::WorldId;
|
||||||
|
|
||||||
/// A data structure for fast spatial queries on entity [hitboxes]. This is used
|
/// A data structure for fast spatial queries on entity [hitboxes]. This is used
|
||||||
|
@ -25,9 +28,9 @@ impl SpatialIndex {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
#[deprecated = "This is for documentation tests only"]
|
#[deprecated = "This is for documentation tests only!"]
|
||||||
pub fn example_new() -> Self {
|
pub fn example_new() -> Self {
|
||||||
println!("Don't call me!");
|
dbg!("Don't call me from outside tests!");
|
||||||
Self::new()
|
Self::new()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,86 +72,157 @@ impl SpatialIndex {
|
||||||
C: FnMut(Aabb<f64>) -> bool,
|
C: FnMut(Aabb<f64>) -> bool,
|
||||||
F: FnMut(EntityId, Aabb<f64>) -> Option<T>,
|
F: FnMut(EntityId, Aabb<f64>) -> Option<T>,
|
||||||
{
|
{
|
||||||
self.traverse(|e, bb| {
|
fn query_rec<C, F, T>(node: Node<EntityId>, collides: &mut C, f: &mut F) -> Option<T>
|
||||||
if collides(bb) {
|
|
||||||
e.and_then(|id| f(id, bb))
|
|
||||||
.map_or(TraverseStep::Hit, TraverseStep::Return)
|
|
||||||
} else {
|
|
||||||
TraverseStep::Miss
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: accept predicate here. Might want to skip invisible entities, for
|
|
||||||
// instance.
|
|
||||||
pub fn raycast(&self, origin: Vec3<f64>, direction: Vec3<f64>) -> Option<RaycastHit> {
|
|
||||||
debug_assert!(
|
|
||||||
direction.is_normalized(),
|
|
||||||
"the ray direction must be normalized"
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut hit: Option<RaycastHit> = None;
|
|
||||||
|
|
||||||
self.traverse::<_, ()>(|entity, bb| {
|
|
||||||
if let Some((near, far)) = ray_box_intersection(origin, direction, bb) {
|
|
||||||
if hit.as_ref().map_or(true, |hit| near < hit.near) {
|
|
||||||
if let Some(entity) = entity {
|
|
||||||
hit = Some(RaycastHit {
|
|
||||||
entity,
|
|
||||||
bb,
|
|
||||||
near,
|
|
||||||
far,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
TraverseStep::Hit
|
|
||||||
} else {
|
|
||||||
// Do not explore subtrees that cannot produce an intersection closer than the
|
|
||||||
// closest we've seen so far.
|
|
||||||
TraverseStep::Miss
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
TraverseStep::Miss
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
hit
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn raycast_all<F, T>(&self, origin: Vec3<f64>, direction: Vec3<f64>, mut f: F) -> Option<T>
|
|
||||||
where
|
where
|
||||||
F: FnMut(RaycastHit) -> Option<T>,
|
C: FnMut(Aabb<f64>) -> bool,
|
||||||
|
F: FnMut(EntityId, Aabb<f64>) -> Option<T>,
|
||||||
{
|
{
|
||||||
debug_assert!(
|
match node {
|
||||||
direction.is_normalized(),
|
Node::Internal(int) => {
|
||||||
"the ray direction must be normalized"
|
let (bb, left, right) = int.split();
|
||||||
);
|
|
||||||
|
|
||||||
self.traverse(
|
if collides(bb) {
|
||||||
|entity, bb| match (ray_box_intersection(origin, direction, bb), entity) {
|
query_rec(left, collides, f).or_else(|| query_rec(right, collides, f))
|
||||||
(Some((near, far)), Some(entity)) => {
|
} else {
|
||||||
let hit = RaycastHit {
|
None
|
||||||
entity,
|
}
|
||||||
|
}
|
||||||
|
Node::Leaf { data, bb } => {
|
||||||
|
if collides(bb) {
|
||||||
|
f(*data, bb)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
query_rec(self.bvh.traverse()?, &mut collides, &mut f)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Casts a ray defined by `origin` and `direction` through entity hitboxes
|
||||||
|
/// and returns the closest intersection for which `f` returns `true`.
|
||||||
|
///
|
||||||
|
/// `f` is a predicate which can be used to filter intersections. For
|
||||||
|
/// instance, if a ray is shot from a player's eye position, you probably
|
||||||
|
/// don't want the ray to intersect with the player's own hitbox.
|
||||||
|
///
|
||||||
|
/// If no intersections are found or if `f` never returns `true` then `None`
|
||||||
|
/// is returned. Additionally, the given ray direction must be
|
||||||
|
/// normalized.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # #[allow(deprecated)]
|
||||||
|
/// # let si = valence::spatial_index::SpatialIndex::example_new();
|
||||||
|
/// use valence::vek::*;
|
||||||
|
///
|
||||||
|
/// let origin = Vec3::new(0.0, 0.0, 0.0);
|
||||||
|
/// let direction = Vec3::new(1.0, 1.0, 1.0).normalized();
|
||||||
|
///
|
||||||
|
/// // Assume `si` is the spatial index.
|
||||||
|
/// if let Some(hit) = si.raycast(origin, direction, |_| true) {
|
||||||
|
/// println!("Raycast hit! {hit:?}");
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub fn raycast<F>(
|
||||||
|
&self,
|
||||||
|
origin: Vec3<f64>,
|
||||||
|
direction: Vec3<f64>,
|
||||||
|
mut f: F,
|
||||||
|
) -> Option<RaycastHit>
|
||||||
|
where
|
||||||
|
F: FnMut(&RaycastHit) -> bool,
|
||||||
|
{
|
||||||
|
fn raycast_rec(
|
||||||
|
node: Node<EntityId>,
|
||||||
|
hit: &mut Option<RaycastHit>,
|
||||||
|
near: f64,
|
||||||
|
far: f64,
|
||||||
|
origin: Vec3<f64>,
|
||||||
|
direction: Vec3<f64>,
|
||||||
|
f: &mut impl FnMut(&RaycastHit) -> bool,
|
||||||
|
) {
|
||||||
|
if let Some(hit) = hit {
|
||||||
|
if hit.near <= near {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match node {
|
||||||
|
Node::Internal(int) => {
|
||||||
|
let (_, left, right) = int.split();
|
||||||
|
|
||||||
|
let int_left = ray_box_intersect(origin, direction, left.bb());
|
||||||
|
let int_right = ray_box_intersect(origin, direction, right.bb());
|
||||||
|
|
||||||
|
match (int_left, int_right) {
|
||||||
|
(Some((near_left, far_left)), Some((near_right, far_right))) => {
|
||||||
|
// Explore closest subtree first.
|
||||||
|
if near_left < near_right {
|
||||||
|
raycast_rec(left, hit, near_left, far_left, origin, direction, f);
|
||||||
|
raycast_rec(
|
||||||
|
right, hit, near_right, far_right, origin, direction, f,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
raycast_rec(
|
||||||
|
right, hit, near_right, far_right, origin, direction, f,
|
||||||
|
);
|
||||||
|
raycast_rec(left, hit, near_left, far_left, origin, direction, f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(Some((near, far)), None) => {
|
||||||
|
raycast_rec(left, hit, near, far, origin, direction, f)
|
||||||
|
}
|
||||||
|
(None, Some((near, far))) => {
|
||||||
|
raycast_rec(right, hit, near, far, origin, direction, f)
|
||||||
|
}
|
||||||
|
(None, None) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Node::Leaf { data, bb } => {
|
||||||
|
let this_hit = RaycastHit {
|
||||||
|
entity: *data,
|
||||||
bb,
|
bb,
|
||||||
near,
|
near,
|
||||||
far,
|
far,
|
||||||
};
|
};
|
||||||
f(hit).map_or(TraverseStep::Hit, TraverseStep::Return)
|
|
||||||
|
if f(&this_hit) {
|
||||||
|
*hit = Some(this_hit);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
(Some(_), None) => TraverseStep::Hit,
|
|
||||||
(None, _) => TraverseStep::Miss,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Explores the spatial index according to `f`.
|
debug_assert!(
|
||||||
///
|
direction.is_normalized(),
|
||||||
/// This is a low-level function that should only be used if the other
|
"the ray direction must be normalized"
|
||||||
/// methods on this type are too restrictive.
|
);
|
||||||
pub fn traverse<F, T>(&self, mut f: F) -> Option<T>
|
|
||||||
where
|
let root = self.bvh.traverse()?;
|
||||||
F: FnMut(Option<EntityId>, Aabb<f64>) -> TraverseStep<T>,
|
let (near, far) = ray_box_intersect(origin, direction, root.bb())?;
|
||||||
{
|
|
||||||
self.bvh.traverse(|e, bb| f(e.cloned(), bb))
|
let mut hit = None;
|
||||||
|
raycast_rec(root, &mut hit, near, far, origin, direction, &mut f);
|
||||||
|
hit
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an iterator over all entities and their hitboxes in
|
||||||
|
/// an unspecified order.
|
||||||
|
pub fn iter(
|
||||||
|
&self,
|
||||||
|
) -> impl ExactSizeIterator<Item = (EntityId, Aabb<f64>)> + FusedIterator + Clone + '_ {
|
||||||
|
self.bvh.iter().map(|(&id, bb)| (id, bb))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a parallel iterator over all entities and their
|
||||||
|
/// hitboxes in an unspecified order.
|
||||||
|
pub fn par_iter(
|
||||||
|
&self,
|
||||||
|
) -> impl IndexedParallelIterator<Item = (EntityId, Aabb<f64>)> + Clone + '_ {
|
||||||
|
self.bvh.par_iter().map(|(&id, bb)| (id, bb))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn update(&mut self, entities: &Entities, id: WorldId) {
|
pub(crate) fn update(&mut self, entities: &Entities, id: WorldId) {
|
||||||
|
@ -162,8 +236,8 @@ impl SpatialIndex {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Represents an intersection between a ray and an entity's axis-aligned
|
/// Represents an intersection between a ray and an entity's axis-aligned
|
||||||
/// bounding box.
|
/// bounding box (hitbox).
|
||||||
#[derive(Clone, Copy, PartialEq)]
|
#[derive(Clone, Copy, PartialEq, Debug)]
|
||||||
pub struct RaycastHit {
|
pub struct RaycastHit {
|
||||||
/// The [`EntityId`] of the entity that was hit by the ray.
|
/// The [`EntityId`] of the entity that was hit by the ray.
|
||||||
pub entity: EntityId,
|
pub entity: EntityId,
|
||||||
|
@ -177,75 +251,3 @@ pub struct RaycastHit {
|
||||||
/// represents the point at which the ray exits the bounding box.
|
/// represents the point at which the ray exits the bounding box.
|
||||||
pub far: f64,
|
pub far: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ray_box_intersection(ro: Vec3<f64>, rd: Vec3<f64>, bb: Aabb<f64>) -> Option<(f64, f64)> {
|
|
||||||
let mut near = -f64::INFINITY;
|
|
||||||
let mut far = f64::INFINITY;
|
|
||||||
|
|
||||||
for i in 0..3 {
|
|
||||||
// Rust's definition of min and max properly handle the NaNs that these
|
|
||||||
// computations might produce.
|
|
||||||
let t0 = (bb.min[i] - ro[i]) / rd[i];
|
|
||||||
let t1 = (bb.max[i] - ro[i]) / rd[i];
|
|
||||||
|
|
||||||
near = near.max(t0.min(t1));
|
|
||||||
far = far.min(t0.max(t1));
|
|
||||||
}
|
|
||||||
|
|
||||||
if near <= far && far >= 0.0 {
|
|
||||||
Some((near.max(0.0), far))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn ray_box_edge_cases() {
|
|
||||||
let bb = Aabb {
|
|
||||||
min: Vec3::new(0.0, 0.0, 0.0),
|
|
||||||
max: Vec3::new(1.0, 1.0, 1.0),
|
|
||||||
};
|
|
||||||
|
|
||||||
let ros = [
|
|
||||||
// On a corner
|
|
||||||
Vec3::new(0.0, 0.0, 0.0),
|
|
||||||
// Outside
|
|
||||||
Vec3::new(-0.5, 0.5, -0.5),
|
|
||||||
// In the center
|
|
||||||
Vec3::new(0.5, 0.5, 0.5),
|
|
||||||
// On an edge
|
|
||||||
Vec3::new(0.0, 0.5, 0.0),
|
|
||||||
// On a face
|
|
||||||
Vec3::new(0.0, 0.5, 0.5),
|
|
||||||
// Outside slabs
|
|
||||||
Vec3::new(-2.0, -2.0, -2.0),
|
|
||||||
];
|
|
||||||
|
|
||||||
let rds = [
|
|
||||||
Vec3::new(1.0, 0.0, 0.0),
|
|
||||||
Vec3::new(-1.0, 0.0, 0.0),
|
|
||||||
Vec3::new(0.0, 1.0, 0.0),
|
|
||||||
Vec3::new(0.0, -1.0, 0.0),
|
|
||||||
Vec3::new(0.0, 0.0, 1.0),
|
|
||||||
Vec3::new(0.0, 0.0, -1.0),
|
|
||||||
];
|
|
||||||
|
|
||||||
assert!(rds.iter().all(|d| d.is_normalized()));
|
|
||||||
|
|
||||||
for ro in ros {
|
|
||||||
for rd in rds {
|
|
||||||
if let Some((near, far)) = ray_box_intersection(ro, rd, bb) {
|
|
||||||
assert!(near.is_finite());
|
|
||||||
assert!(far.is_finite());
|
|
||||||
assert!(near <= far);
|
|
||||||
assert!(near >= 0.0);
|
|
||||||
assert!(far >= 0.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -397,6 +397,12 @@ impl<T: Into<Text>> std::ops::AddAssign<T> for Text {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<char> for Text {
|
||||||
|
fn from(c: char) -> Self {
|
||||||
|
Text::text(String::from(c))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<String> for Text {
|
impl From<String> for Text {
|
||||||
fn from(s: String) -> Self {
|
fn from(s: String) -> Self {
|
||||||
Text::text(s)
|
Text::text(s)
|
||||||
|
|
123
src/util.rs
123
src/util.rs
|
@ -62,7 +62,7 @@ where
|
||||||
),
|
),
|
||||||
max: Vec3::new(
|
max: Vec3::new(
|
||||||
bottom.x + size.x / 2.0.as_(),
|
bottom.x + size.x / 2.0.as_(),
|
||||||
bottom.y,
|
bottom.y + size.y,
|
||||||
bottom.z + size.z / 2.0.as_(),
|
bottom.z + size.z / 2.0.as_(),
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
@ -75,19 +75,118 @@ where
|
||||||
/// Takes a normalized direction vector and returns a `(yaw, pitch)` tuple in
|
/// Takes a normalized direction vector and returns a `(yaw, pitch)` tuple in
|
||||||
/// degrees.
|
/// degrees.
|
||||||
///
|
///
|
||||||
// /// This function is the inverse of [`from_yaw_and_pitch`].
|
/// This function is the inverse of [`from_yaw_and_pitch`] except for the case
|
||||||
pub fn to_yaw_and_pitch(d: Vec3<f64>) -> (f32, f32) {
|
/// where the direction is straight up or down.
|
||||||
|
pub fn to_yaw_and_pitch(d: Vec3<f64>) -> (f64, f64) {
|
||||||
debug_assert!(d.is_normalized(), "the given vector should be normalized");
|
debug_assert!(d.is_normalized(), "the given vector should be normalized");
|
||||||
|
|
||||||
let yaw = f32::atan2(d.z as f32, d.x as f32).to_degrees() - 90.0;
|
let yaw = f64::atan2(d.z, d.x).to_degrees() - 90.0;
|
||||||
let pitch = -(d.y as f32).asin().to_degrees();
|
let pitch = -(d.y).asin().to_degrees();
|
||||||
(yaw, pitch)
|
(yaw, pitch)
|
||||||
}
|
}
|
||||||
|
|
||||||
// /// Takes yaw and pitch angles (in degrees) and returns a normalized
|
/// Takes yaw and pitch angles (in degrees) and returns a normalized
|
||||||
// direction /// vector.
|
/// direction vector.
|
||||||
// ///
|
///
|
||||||
// /// This function is the inverse of [`to_yaw_and_pitch`].
|
/// This function is the inverse of [`to_yaw_and_pitch`].
|
||||||
// pub fn from_yaw_and_pitch(yaw: f32, pitch: f32) -> Vec3<f64> {
|
pub fn from_yaw_and_pitch(yaw: f64, pitch: f64) -> Vec3<f64> {
|
||||||
// todo!()
|
let yaw = (yaw + 90.0).to_radians();
|
||||||
// }
|
let pitch = (-pitch).to_radians();
|
||||||
|
|
||||||
|
let xz_len = pitch.cos();
|
||||||
|
Vec3::new(yaw.cos() * xz_len, pitch.sin(), yaw.sin() * xz_len)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculates the intersection between an axis-aligned bounding box and a ray
|
||||||
|
/// defined by its origin `ro` and direction `rd`.
|
||||||
|
///
|
||||||
|
/// If an intersection occurs, `Some((near, far))` is returned. `near` and `far`
|
||||||
|
/// are the distance from the origin to the closest and furthest intersection
|
||||||
|
/// points respectively. If the intersection occurs inside the bounding box,
|
||||||
|
/// then `near` is zero.
|
||||||
|
pub fn ray_box_intersect(ro: Vec3<f64>, rd: Vec3<f64>, bb: Aabb<f64>) -> Option<(f64, f64)> {
|
||||||
|
let mut near = -f64::INFINITY;
|
||||||
|
let mut far = f64::INFINITY;
|
||||||
|
|
||||||
|
for i in 0..3 {
|
||||||
|
// Rust's definition of min and max properly handle the NaNs that these
|
||||||
|
// computations might produce.
|
||||||
|
let t0 = (bb.min[i] - ro[i]) / rd[i];
|
||||||
|
let t1 = (bb.max[i] - ro[i]) / rd[i];
|
||||||
|
|
||||||
|
near = near.max(t0.min(t1));
|
||||||
|
far = far.min(t0.max(t1));
|
||||||
|
}
|
||||||
|
|
||||||
|
if near <= far && far >= 0.0 {
|
||||||
|
Some((near.max(0.0), far))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use approx::assert_relative_eq;
|
||||||
|
use rand::random;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn yaw_pitch_round_trip() {
|
||||||
|
for _ in 0..=100 {
|
||||||
|
let d = (Vec3::new(random(), random(), random()) * 2.0 - 1.0).normalized();
|
||||||
|
|
||||||
|
let (yaw, pitch) = to_yaw_and_pitch(d);
|
||||||
|
let d_new = from_yaw_and_pitch(yaw, pitch);
|
||||||
|
|
||||||
|
assert_relative_eq!(d, d_new, epsilon = f64::EPSILON * 100.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ray_box_edge_cases() {
|
||||||
|
let bb = Aabb {
|
||||||
|
min: Vec3::new(0.0, 0.0, 0.0),
|
||||||
|
max: Vec3::new(1.0, 1.0, 1.0),
|
||||||
|
};
|
||||||
|
|
||||||
|
let ros = [
|
||||||
|
// On a corner
|
||||||
|
Vec3::new(0.0, 0.0, 0.0),
|
||||||
|
// Outside
|
||||||
|
Vec3::new(-0.5, 0.5, -0.5),
|
||||||
|
// In the center
|
||||||
|
Vec3::new(0.5, 0.5, 0.5),
|
||||||
|
// On an edge
|
||||||
|
Vec3::new(0.0, 0.5, 0.0),
|
||||||
|
// On a face
|
||||||
|
Vec3::new(0.0, 0.5, 0.5),
|
||||||
|
// Outside slabs
|
||||||
|
Vec3::new(-2.0, -2.0, -2.0),
|
||||||
|
];
|
||||||
|
|
||||||
|
let rds = [
|
||||||
|
Vec3::new(1.0, 0.0, 0.0),
|
||||||
|
Vec3::new(-1.0, 0.0, 0.0),
|
||||||
|
Vec3::new(0.0, 1.0, 0.0),
|
||||||
|
Vec3::new(0.0, -1.0, 0.0),
|
||||||
|
Vec3::new(0.0, 0.0, 1.0),
|
||||||
|
Vec3::new(0.0, 0.0, -1.0),
|
||||||
|
];
|
||||||
|
|
||||||
|
assert!(rds.iter().all(|d| d.is_normalized()));
|
||||||
|
|
||||||
|
for ro in ros {
|
||||||
|
for rd in rds {
|
||||||
|
if let Some((near, far)) = ray_box_intersect(ro, rd, bb) {
|
||||||
|
assert!(near.is_finite());
|
||||||
|
assert!(far.is_finite());
|
||||||
|
assert!(near <= far);
|
||||||
|
assert!(near >= 0.0);
|
||||||
|
assert!(far >= 0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue