Add CapsulePrism collider variant

+ Add placeholder physics collision implementation as copy of cylinder
  Box collider.
+ Display it with debug hitboxes.
This commit is contained in:
juliancoffee 2021-09-11 15:06:13 +03:00
parent ce29e99403
commit 3b308a3f6f
9 changed files with 461 additions and 97 deletions

View File

@ -9,6 +9,7 @@ layout (std140, set = 1, binding = 0)
uniform u_locals { uniform u_locals {
vec4 w_pos; vec4 w_pos;
vec4 w_color; vec4 w_color;
vec4 w_ori;
}; };
layout (location = 0) layout (location = 0)
@ -16,5 +17,29 @@ out vec4 f_color;
void main() { void main() {
f_color = w_color; f_color = w_color;
gl_Position = all_mat * vec4((v_pos + w_pos.xyz) - focus_off.xyz, 1);
// Build rotation matrix
// https://en.wikipedia.org/wiki/Conversion_between_quaternions_and_Euler_angles#Rotation_matrices
mat3 rotation_matrix;
float q0 = w_ori[3];
float q1 = w_ori[0];
float q2 = w_ori[1];
float q3 = w_ori[2];
float r00 = 1 - 2 * (pow(q2, 2) + pow(q3, 2));
float r01 = 2 * (q1 * q2 - q0 * q3);
float r02 = 2 * (q0 * q2 + q1 * q3);
rotation_matrix[0] = vec3(r00, r01, r02);
float r10 = 2 * (q1 * q2 + q0 * q3);
float r11 = 1 - 2 * (pow(q1, 2) + pow(q3, 2));
float r12 = 2 * (q2 * q3 - q0 * q1);
rotation_matrix[1] = vec3(r10, r11, r12);
float r20 = 2 * (q1 * q3 - q0 * q2);
float r21 = 2 * (q0 * q1 + q2 * q3);
float r22 = 1 - 2 * (pow(q1, 2) + pow(q2, 2));
rotation_matrix[2] = vec3(r20, r21, r22);
gl_Position = all_mat * vec4((v_pos * rotation_matrix + w_pos.xyz) - focus_off.xyz, 1);
} }

View File

@ -147,6 +147,20 @@ impl<
const EXTENSION: &'static str = "ron"; const EXTENSION: &'static str = "ron";
} }
// Utility enum used to build Stadium points
// Read doc for [Body::sausage] for more.
//
// Actually can be removed I guess?
// We can just determine shape form dimensions.
//
// But I want Dachshund in Veloren at least somewhere XD
enum Shape {
// Dachshund-like
Long,
// Cyclops-like
Wide,
}
impl Body { impl Body {
pub fn is_humanoid(&self) -> bool { matches!(self, Body::Humanoid(_)) } pub fn is_humanoid(&self) -> bool { matches!(self, Body::Humanoid(_)) }
@ -292,11 +306,12 @@ impl Body {
} }
/// The width (shoulder to shoulder), length (nose to tail) and height /// The width (shoulder to shoulder), length (nose to tail) and height
/// respectively /// respectively (in metres)
// Code reviewers: should we replace metres with 'block height'?
pub fn dimensions(&self) -> Vec3<f32> { pub fn dimensions(&self) -> Vec3<f32> {
match self { match self {
Body::BipedLarge(body) => match body.species { Body::BipedLarge(body) => match body.species {
biped_large::Species::Cyclops => Vec3::new(4.6, 3.0, 6.5), biped_large::Species::Cyclops => Vec3::new(5.6, 3.0, 6.5),
biped_large::Species::Dullahan => Vec3::new(4.6, 3.0, 5.5), biped_large::Species::Dullahan => Vec3::new(4.6, 3.0, 5.5),
biped_large::Species::Mightysaurok => Vec3::new(4.0, 3.0, 3.4), biped_large::Species::Mightysaurok => Vec3::new(4.0, 3.0, 3.4),
biped_large::Species::Mindflayer => Vec3::new(4.4, 3.0, 8.0), biped_large::Species::Mindflayer => Vec3::new(4.4, 3.0, 8.0),
@ -350,6 +365,8 @@ impl Body {
quadruped_medium::Species::Ngoubou => Vec3::new(2.0, 3.2, 2.4), quadruped_medium::Species::Ngoubou => Vec3::new(2.0, 3.2, 2.4),
quadruped_medium::Species::Llama => Vec3::new(2.0, 2.5, 2.6), quadruped_medium::Species::Llama => Vec3::new(2.0, 2.5, 2.6),
quadruped_medium::Species::Alpaca => Vec3::new(2.0, 2.0, 2.0), quadruped_medium::Species::Alpaca => Vec3::new(2.0, 2.0, 2.0),
quadruped_medium::Species::Camel => Vec3::new(2.0, 4.0, 3.5),
// FIXME: We really shouldn't be doing wildcards here
_ => Vec3::new(2.0, 3.0, 2.0), _ => Vec3::new(2.0, 3.0, 2.0),
}, },
Body::QuadrupedSmall(body) => match body.species { Body::QuadrupedSmall(body) => match body.species {
@ -386,6 +403,26 @@ impl Body {
} }
} }
fn shape(&self) -> Shape {
match self {
Body::BipedLarge(_)
| Body::BipedSmall(_)
| Body::Golem(_)
| Body::Humanoid(_)
| Body::Object(_) => Shape::Wide,
Body::BirdLarge(_)
| Body::BirdMedium(_)
| Body::Dragon(_)
| Body::FishMedium(_)
| Body::FishSmall(_)
| Body::QuadrupedLow(_)
| Body::QuadrupedMedium(_)
| Body::QuadrupedSmall(_)
| Body::Ship(_)
| Body::Theropod(_) => Shape::Long,
}
}
// Note: This is used for collisions, but it's not very accurate for shapes that // Note: This is used for collisions, but it's not very accurate for shapes that
// are very much not cylindrical. Eventually this ought to be replaced by more // are very much not cylindrical. Eventually this ought to be replaced by more
// accurate collision shapes. // accurate collision shapes.
@ -394,6 +431,52 @@ impl Body {
dim.x.max(dim.y) / 2.0 dim.x.max(dim.y) / 2.0
} }
/// Base of our Capsule Prism used for collisions.
/// Returns line segment and radius. See [this wiki page][stadium_wiki].
///
/// [stadium_wiki]: <https://en.wikipedia.org/wiki/Stadium_(geometry)>
pub fn sausage(&self) -> (Vec2<f32>, Vec2<f32>, f32) {
// Consider this ascii-art stadium with radius `r` and line segment `a`
//
// xxxxxxxxxxxxxxxxx
//
// _ aaaaaaaaa
// y -* r * -
// y * r *
// y * rrr --------- rrr *
// y * r *
// y * r *
// *__aaaaaaaaa_ ^
let dim = self.dimensions();
// The width (shoulder to shoulder) and length (nose to tail)
let (width, length) = (dim.x, dim.y);
match self.shape() {
Shape::Long => {
let radius = width / 2.0;
let a = length - 2.0 * radius;
debug_assert!(a > 0.0);
let p0 = Vec2::new(0.0, -a / 2.0);
let p1 = Vec2::new(0.0, a / 2.0);
(p0, p1, radius)
},
Shape::Wide => {
let radius = length / 2.0;
let a = width - 2.0 * radius;
debug_assert!(a > 0.0);
let p0 = Vec2::new(-a / 2.0, 0.0);
let p1 = Vec2::new(a / 2.0, 0.0);
(p0, p1, radius)
},
}
}
// How far away other entities should try to be. Will be added uppon the other // How far away other entities should try to be. Will be added uppon the other
// entitys spacing_radius. So an entity with 2.0 and an entity with 3.0 will // entitys spacing_radius. So an entity with 2.0 and an entity with 3.0 will
// lead to that both entities will try to keep 5.0 units away from each // lead to that both entities will try to keep 5.0 units away from each
@ -417,6 +500,7 @@ impl Body {
} }
} }
/// Height from the bottom to the top (in metres)
pub fn height(&self) -> f32 { self.dimensions().z } pub fn height(&self) -> f32 { self.dimensions().z }
pub fn base_energy(&self) -> u32 { pub fn base_energy(&self) -> u32 {

View File

@ -99,10 +99,24 @@ impl Component for Density {
// Collider // Collider
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum Collider { pub enum Collider {
// TODO: pass the map from ids -> voxel data to get_radius and get_z_limits to compute a // TODO: pass the map from ids -> voxel data to get_radius
// bounding cylinder // and get_z_limits to compute a bounding cylinder.
Voxel { id: String }, Voxel {
Box { radius: f32, z_min: f32, z_max: f32 }, id: String,
},
Box {
radius: f32,
z_min: f32,
z_max: f32,
},
/// Capsule prism with line segment from p0 to p1
CapsulePrism {
p0: Vec2<f32>,
p1: Vec2<f32>,
radius: f32,
z_min: f32,
z_max: f32,
},
Point, Point,
} }
@ -111,6 +125,11 @@ impl Collider {
match self { match self {
Collider::Voxel { .. } => 1.0, Collider::Voxel { .. } => 1.0,
Collider::Box { radius, .. } => *radius, Collider::Box { radius, .. } => *radius,
// FIXME: I know that this is wrong for sure,
// because it's not a circle.
//
// CodeReviewers, please welp!
Collider::CapsulePrism { radius, .. } => *radius,
Collider::Point => 0.0, Collider::Point => 0.0,
} }
} }
@ -124,6 +143,7 @@ impl Collider {
match self { match self {
Collider::Voxel { .. } => (0.0, 1.0), Collider::Voxel { .. } => (0.0, 1.0),
Collider::Box { z_min, z_max, .. } => (*z_min * modifier, *z_max * modifier), Collider::Box { z_min, z_max, .. } => (*z_min * modifier, *z_max * modifier),
Collider::CapsulePrism { z_min, z_max, .. } => (*z_min * modifier, *z_max * modifier),
Collider::Point => (0.0, 0.0), Collider::Point => (0.0, 0.0),
} }
} }

View File

@ -290,8 +290,9 @@ impl<'a> PhysicsData<'a> {
!&read.mountings, !&read.mountings,
read.stickies.maybe(), read.stickies.maybe(),
&mut write.physics_states, &mut write.physics_states,
// TODO: if we need to avoid collisions for other things consider moving whether it // TODO: if we need to avoid collisions for other things consider
// should interact into the collider component or into a separate component // moving whether it should interact into the collider component
// or into a separate component.
read.projectiles.maybe(), read.projectiles.maybe(),
read.char_states.maybe(), read.char_states.maybe(),
) )
@ -322,9 +323,10 @@ impl<'a> PhysicsData<'a> {
let mut entity_entity_collision_checks = 0; let mut entity_entity_collision_checks = 0;
let mut entity_entity_collisions = 0; let mut entity_entity_collisions = 0;
// TODO: quick fix for bad performance at extrememly high velocities // TODO: quick fix for bad performance. At extrememly high
// use oriented rectangles at some threshold of displacement/radius // velocities use oriented rectangles at some threshold of
// to query the spatial grid and limit max displacement per tick somehow // displacement/radius to query the spatial grid and limit
// max displacement per tick somehow.
if previous_cache.collision_boundary > 128.0 { if previous_cache.collision_boundary > 128.0 {
return PhysicsMetrics { return PhysicsMetrics {
entity_entity_collision_checks, entity_entity_collision_checks,
@ -419,9 +421,14 @@ impl<'a> PhysicsData<'a> {
<= pos_other.z <= pos_other.z
+ z_limits_other.1 * previous_cache_other.scale + z_limits_other.1 * previous_cache_other.scale
{ {
// If entities have not yet collided this tick (but just // If entities have not yet collided
// did) and if entity is either in mid air or is not sticky, // this tick (but just did) and if entity
// then mark them as colliding with the other entity //
// is either in mid air
// or is not sticky,
//
// then mark them as colliding with the
// other entity.
if !collision_registered && (is_mid_air || !is_sticky) { if !collision_registered && (is_mid_air || !is_sticky) {
physics.touch_entities.insert(*other); physics.touch_entities.insert(*other);
entity_entity_collisions += 1; entity_entity_collisions += 1;
@ -627,8 +634,9 @@ impl<'a> PhysicsData<'a> {
// And not already stuck on a block (e.g., for arrows) // And not already stuck on a block (e.g., for arrows)
&& !(physics_state.on_surface().is_some() && sticky.is_some()) && !(physics_state.on_surface().is_some() && sticky.is_some())
{ {
// Clamp dt to an effective 10 TPS, to prevent gravity from slamming the // Clamp dt to an effective 10 TPS, to prevent gravity
// players into the floor when stationary if other systems cause the server // from slamming the players into the floor when
// stationary if other systems cause the server
// to lag (as observed in the 0.9 release party). // to lag (as observed in the 0.9 release party).
let dt = DeltaTime(read.dt.0.min(0.1)); let dt = DeltaTime(read.dt.0.min(0.1));
@ -750,8 +758,8 @@ impl<'a> PhysicsData<'a> {
)| { )| {
let mut land_on_ground = None; let mut land_on_ground = None;
let mut outcomes = Vec::new(); let mut outcomes = Vec::new();
// Defer the writes of positions, velocities and orientations to allow an inner // Defer the writes of positions, velocities and orientations
// loop over terrain-like entities // to allow an inner loop over terrain-like entities.
let old_vel = *vel; let old_vel = *vel;
let mut vel = *vel; let mut vel = *vel;
let old_ori = *ori; let old_ori = *ori;
@ -777,17 +785,23 @@ impl<'a> PhysicsData<'a> {
Vec3::zero() Vec3::zero()
}; };
// What's going on here? Because collisions need to be resolved against multiple // What's going on here?
// Because collisions need to be resolved against multiple
// colliders, this code takes the current position and // colliders, this code takes the current position and
// propagates it forward according to velocity to find a // propagates it forward according to velocity to find a
// 'target' position. This is where we'd ideally end up at the end of the tick, // 'target' position.
//
// This is where we'd ideally end up at the end of the tick,
// assuming no collisions. Then, we refine this target by // assuming no collisions. Then, we refine this target by
// stepping from the original position to the target for // stepping from the original position to the target for
// every obstacle, refining the target position as we go. It's not perfect, but // every obstacle, refining the target position as we go.
// it works pretty well in practice. Oddities can occur on //
// the intersection between multiple colliders, but it's not // It's not perfect, but it works pretty well in practice.
// like any game physics system resolves these sort of things well anyway. At // Oddities can occur on the intersection between multiple
// the very least, we don't do things that result in glitchy // colliders, but it's not like any game physics system
// resolves these sort of things well anyway.
//
// At the very least, we don't do things that result in glitchy
// velocities or entirely broken position snapping. // velocities or entirely broken position snapping.
let mut tgt_pos = pos.0 + pos_delta; let mut tgt_pos = pos.0 + pos_delta;
@ -799,11 +813,12 @@ impl<'a> PhysicsData<'a> {
match &collider { match &collider {
Collider::Voxel { .. } => { Collider::Voxel { .. } => {
// for now, treat entities with voxel colliders as their bounding // For now, treat entities with voxel colliders
// cylinders for the purposes of colliding them with terrain // as their bounding cylinders for the purposes of
// colliding them with terrain.
// Additionally, multiply radius by 0.1 to make the cylinder smaller to //
// avoid lag // Additionally, multiply radius by 0.1 to make
// the cylinder smaller to avoid lag.
let radius = collider.get_radius() * scale * 0.1; let radius = collider.get_radius() * scale * 0.1;
let (z_min, z_max) = collider.get_z_limits(scale); let (z_min, z_max) = collider.get_z_limits(scale);
@ -832,6 +847,50 @@ impl<'a> PhysicsData<'a> {
z_min, z_min,
z_max, z_max,
} => { } => {
// FIXME:
// !! DEPRECATED !!
//
// Scale collider
let radius = radius.min(0.45) * scale;
let z_min = *z_min * scale;
let z_max = z_max.clamped(1.2, 1.95) * scale;
let cylinder = (radius, z_min, z_max);
let mut cpos = *pos;
box_voxel_collision(
cylinder,
&*read.terrain,
entity,
&mut cpos,
tgt_pos,
&mut vel,
&mut physics_state,
Vec3::zero(),
&read.dt,
was_on_ground,
block_snap,
climbing,
|entity, vel| land_on_ground = Some((entity, vel)),
read,
);
// Sticky things shouldn't move when on a surface
if physics_state.on_surface().is_some() && sticky.is_some() {
vel.0 = physics_state.ground_vel;
}
tgt_pos = cpos.0;
},
Collider::CapsulePrism {
radius,
z_min,
z_max,
..
} => {
// FIXME: placeholder.
// copypasted from Box collider
//
// This shoudln't pass Code Review.
// Scale collider // Scale collider
let radius = radius.min(0.45) * scale; let radius = radius.min(0.45) * scale;
let z_min = *z_min * scale; let z_min = *z_min * scale;
@ -866,8 +925,10 @@ impl<'a> PhysicsData<'a> {
Collider::Point => { Collider::Point => {
let mut pos = *pos; let mut pos = *pos;
// If the velocity is exactly 0, a raycast may not pick up the current // TODO: If the velocity is exactly 0,
// block. Handle this. // a raycast may not pick up the current block.
//
// Handle this.
let (dist, block) = if let Some(block) = read let (dist, block) = if let Some(block) = read
.terrain .terrain
.get(pos.0.map(|e| e.floor() as i32)) .get(pos.0.map(|e| e.floor() as i32))
@ -882,7 +943,8 @@ impl<'a> PhysicsData<'a> {
.until(|block: &Block| block.is_solid()) .until(|block: &Block| block.is_solid())
.ignore_error() .ignore_error()
.cast(); .cast();
(dist, block.unwrap()) // Can't fail since we do ignore_error above // Can't fail since we do ignore_error above
(dist, block.unwrap())
}; };
pos.0 += pos_delta.try_normalized().unwrap_or_else(Vec3::zero) * dist; pos.0 += pos_delta.try_normalized().unwrap_or_else(Vec3::zero) * dist;
@ -987,6 +1049,7 @@ impl<'a> PhysicsData<'a> {
// Collide with terrain-like entities // Collide with terrain-like entities
let query_center = path_sphere.center.xy(); let query_center = path_sphere.center.xy();
let query_radius = path_sphere.radius; let query_radius = path_sphere.radius;
voxel_collider_spatial_grid voxel_collider_spatial_grid
.in_circle_aabr(query_center, query_radius) .in_circle_aabr(query_center, query_radius)
.filter_map(|entity| { .filter_map(|entity| {
@ -1280,6 +1343,7 @@ impl<'a> System<'a> for Sys {
} }
} }
#[warn(clippy::pedantic)]
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
fn box_voxel_collision<'a, T: BaseVol<Vox = Block> + ReadVol>( fn box_voxel_collision<'a, T: BaseVol<Vox = Block> + ReadVol>(
cylinder: (f32, f32, f32), // effective collision cylinder cylinder: (f32, f32, f32), // effective collision cylinder
@ -1297,22 +1361,6 @@ fn box_voxel_collision<'a, T: BaseVol<Vox = Block> + ReadVol>(
mut land_on_ground: impl FnMut(Entity, Vel), mut land_on_ground: impl FnMut(Entity, Vel),
read: &PhysicsRead, read: &PhysicsRead,
) { ) {
let (radius, z_min, z_max) = cylinder;
// Probe distances
let hdist = radius.ceil() as i32;
// Neighbouring blocks iterator
let near_iter = (-hdist..hdist + 1)
.map(move |i| {
(-hdist..hdist + 1).map(move |j| {
(1 - Block::MAX_HEIGHT.ceil() as i32 + z_min.floor() as i32
..z_max.ceil() as i32 + 1)
.map(move |k| (i, j, k))
})
})
.flatten()
.flatten();
// Function for iterating over the blocks the player at a specific position // Function for iterating over the blocks the player at a specific position
// collides with // collides with
fn collision_iter<'a, T: BaseVol<Vox = Block> + ReadVol>( fn collision_iter<'a, T: BaseVol<Vox = Block> + ReadVol>(
@ -1357,7 +1405,6 @@ fn box_voxel_collision<'a, T: BaseVol<Vox = Block> + ReadVol>(
}) })
} }
let z_range = z_min..z_max;
// Function for determining whether the player at a specific position collides // Function for determining whether the player at a specific position collides
// with blocks with the given criteria // with blocks with the given criteria
fn collision_with<'a, T: BaseVol<Vox = Block> + ReadVol>( fn collision_with<'a, T: BaseVol<Vox = Block> + ReadVol>(
@ -1381,6 +1428,25 @@ fn box_voxel_collision<'a, T: BaseVol<Vox = Block> + ReadVol>(
.is_some() .is_some()
} }
fn always_hits(_: &Block) -> bool { true }
let (radius, z_min, z_max) = cylinder;
// Probe distances
let hdist = radius.ceil() as i32;
// Neighbouring blocks iterator
let near_iter = (-hdist..hdist + 1)
.map(move |i| {
(-hdist..hdist + 1).map(move |j| {
(1 - Block::MAX_HEIGHT.ceil() as i32 + z_min.floor() as i32
..z_max.ceil() as i32 + 1)
.map(move |k| (i, j, k))
})
})
.flatten()
.flatten();
let z_range = z_min..z_max;
physics_state.on_ground = None; physics_state.on_ground = None;
physics_state.on_ceiling = false; physics_state.on_ceiling = false;
@ -1395,7 +1461,6 @@ fn box_voxel_collision<'a, T: BaseVol<Vox = Block> + ReadVol>(
.ceil() .ceil()
.max(1.0); .max(1.0);
let old_pos = pos.0; let old_pos = pos.0;
fn block_true(_: &Block) -> bool { true }
for _ in 0..increments as usize { for _ in 0..increments as usize {
pos.0 += pos_delta / increments; pos.0 += pos_delta / increments;
@ -1485,7 +1550,7 @@ fn box_voxel_collision<'a, T: BaseVol<Vox = Block> + ReadVol>(
// ...and the vertical resolution direction is sufficiently great... // ...and the vertical resolution direction is sufficiently great...
&& dir.z < -0.1 && dir.z < -0.1
// ...and the space above is free... // ...and the space above is free...
&& !collision_with(Vec3::new(pos.0.x, pos.0.y, (pos.0.z + 0.1).ceil()), &terrain, block_true, near_iter.clone(), radius, z_range.clone()) && !collision_with(Vec3::new(pos.0.x, pos.0.y, (pos.0.z + 0.1).ceil()), &terrain, always_hits, near_iter.clone(), radius, z_range.clone())
// ...and we're falling/standing OR there is a block *directly* beneath our current origin (note: not hitbox)... // ...and we're falling/standing OR there is a block *directly* beneath our current origin (note: not hitbox)...
// && terrain // && terrain
// .get((pos.0 - Vec3::unit_z() * 0.1).map(|e| e.floor() as i32)) // .get((pos.0 - Vec3::unit_z() * 0.1).map(|e| e.floor() as i32))
@ -1495,7 +1560,7 @@ fn box_voxel_collision<'a, T: BaseVol<Vox = Block> + ReadVol>(
&& collision_with( && collision_with(
pos.0 + resolve_dir - Vec3::unit_z() * 1.25, pos.0 + resolve_dir - Vec3::unit_z() * 1.25,
&terrain, &terrain,
block_true, always_hits,
near_iter.clone(), near_iter.clone(),
radius, radius,
z_range.clone(), z_range.clone(),
@ -1545,7 +1610,7 @@ fn box_voxel_collision<'a, T: BaseVol<Vox = Block> + ReadVol>(
} else if collision_with( } else if collision_with(
pos.0 - Vec3::unit_z() * 1.1, pos.0 - Vec3::unit_z() * 1.1,
&terrain, &terrain,
block_true, always_hits,
near_iter.clone(), near_iter.clone(),
radius, radius,
z_range.clone(), z_range.clone(),

View File

@ -185,6 +185,7 @@ impl StateExt for State {
inventory: comp::Inventory, inventory: comp::Inventory,
body: comp::Body, body: comp::Body,
) -> EcsEntityBuilder { ) -> EcsEntityBuilder {
use comp::body::{biped_large, quadruped_medium};
self.ecs_mut() self.ecs_mut()
.create_entity_synced() .create_entity_synced()
.with(pos) .with(pos)
@ -203,6 +204,31 @@ impl StateExt for State {
comp::Body::Ship(ship) => comp::Collider::Voxel { comp::Body::Ship(ship) => comp::Collider::Voxel {
id: ship.manifest_entry().to_string(), id: ship.manifest_entry().to_string(),
}, },
body
@
(comp::Body::QuadrupedMedium(quadruped_medium::Body {
species: quadruped_medium::Species::Camel,
..
})
| comp::Body::BipedLarge(biped_large::Body {
species: biped_large::Species::Cyclops,
..
})) => {
// no Camel was hurt while doing this sausage
// can't say the same about Cyclops
let (p0, p1, radius) = body.sausage();
// TODO:
// It would be cool not have z_min as hardcoded 0.0
// but it needs to work nicer with terrain collisions.
comp::Collider::CapsulePrism {
p0,
p1,
radius,
z_min: 0.0,
z_max: body.height(),
}
},
_ => comp::Collider::Box { _ => comp::Collider::Box {
radius: body.radius(), radius: body.radius(),
z_min: 0.0, z_min: 0.0,

View File

@ -36,6 +36,8 @@ pub struct Locals {
/// by the shader /// by the shader
pub pos: [f32; 4], pub pos: [f32; 4],
pub color: [f32; 4], pub color: [f32; 4],
/// quaternion as [x, y, z, w]
pub ori: [f32; 4],
} }
pub type BoundLocals = Bound<Consts<Locals>>; pub type BoundLocals = Bound<Consts<Locals>>;

View File

@ -9,7 +9,16 @@ use vek::*;
#[derive(Debug)] #[derive(Debug)]
pub enum DebugShape { pub enum DebugShape {
Line([Vec3<f32>; 2]), Line([Vec3<f32>; 2]),
Cylinder { radius: f32, height: f32 }, Cylinder {
radius: f32,
height: f32,
},
CapsulePrism {
p0: Vec2<f32>,
p1: Vec2<f32>,
radius: f32,
height: f32,
},
} }
impl DebugShape { impl DebugShape {
@ -22,28 +31,117 @@ impl DebugShape {
let quad = |x: Vec3<f32>, y: Vec3<f32>, z: Vec3<f32>, w: Vec3<f32>| { let quad = |x: Vec3<f32>, y: Vec3<f32>, z: Vec3<f32>, w: Vec3<f32>| {
Quad::<DebugVertex>::new(x.into(), y.into(), z.into(), w.into()) Quad::<DebugVertex>::new(x.into(), y.into(), z.into(), w.into())
}; };
match self { match self {
DebugShape::Line([a, b]) => { DebugShape::Line([a, b]) => {
let h = Vec3::new(0.0, 1.0, 0.0); let h = Vec3::new(0.0, 1.0, 0.0);
mesh.push_quad(quad(*a, a + h, b + h, *b)); mesh.push_quad(quad(*a, a + h, b + h, *b));
}, },
DebugShape::Cylinder { radius, height } => { DebugShape::Cylinder { radius, height } => {
const SUBDIVISIONS: usize = 16; const SUBDIVISIONS: u8 = 16;
for i in 0..SUBDIVISIONS { for i in 0..SUBDIVISIONS {
let angle = |j: usize| (j as f32 / SUBDIVISIONS as f32) * 2.0 * PI; // dot on circle edge
let a = Vec3::zero(); let to = |n: u8| {
let b = Vec3::new(radius * angle(i).cos(), radius * angle(i).sin(), 0.0); const FULL: f32 = 2.0 * PI;
let c = Vec3::new( let angle = FULL * f32::from(n) / f32::from(SUBDIVISIONS);
radius * angle(i + 1).cos(),
radius * angle(i + 1).sin(), Vec3::new(radius * angle.cos(), radius * angle.sin(), 0.0)
0.0, };
);
let origin = Vec3::zero();
let r0 = to(i);
let r1 = to(i + 1);
let h = Vec3::new(0.0, 0.0, *height); let h = Vec3::new(0.0, 0.0, *height);
mesh.push_tri(tri(c, b, a));
mesh.push_quad(quad(b, c, c + h, b + h)); // Draw bottom sector
mesh.push_tri(tri(a + h, b + h, c + h)); mesh.push_tri(tri(r1, r0, origin));
// Draw face
mesh.push_quad(quad(r0, r1, r1 + h, r0 + h));
// Draw top sector
mesh.push_tri(tri(origin + h, r0 + h, r1 + h));
} }
}, },
DebugShape::CapsulePrism {
p0,
p1,
radius,
height,
} => {
// We split circle in two parts
const HALF_SECTORS: u8 = 8;
const TOTAL: u8 = HALF_SECTORS * 2;
let offset = (p0 - p1).angle_between(Vec2::new(0.0, 1.0));
let h = Vec3::new(0.0, 0.0, *height);
let draw_cylinder_sector =
|mesh: &mut Mesh<DebugVertex>, origin: Vec3<f32>, from: u8, to: u8| {
for i in from..to {
// dot on circle edge
let to = |n: u8| {
const FULL: f32 = 2.0 * PI;
let angle = offset + FULL * f32::from(n) / f32::from(TOTAL);
let (x, y) = (radius * angle.cos(), radius * angle.sin());
let to_edge = Vec3::new(x, y, 0.0);
origin + to_edge
};
let r0 = to(i);
let r1 = to(i + 1);
// Draw bottom sector
mesh.push_tri(tri(r1, r0, origin));
// Draw face
mesh.push_quad(quad(r0, r1, r1 + h, r0 + h));
// Draw top sector
mesh.push_tri(tri(origin + h, r0 + h, r1 + h));
}
};
let p0 = Vec3::new(p0.x, p0.y, 0.0);
let p1 = Vec3::new(p1.x, p1.y, 0.0);
// 1) Draw first half-cylinder
draw_cylinder_sector(&mut mesh, p0, 0, HALF_SECTORS);
// 2) Draw cuboid in-between
// get main line segment
let a = p1 - p0;
// normalize
let a = a / a.magnitude();
// stretch to radius
let a = a * *radius;
// rotate to 90 degrees to get needed shift
let ortoghonal = Quaternion::rotation_z(PI / 2.0);
let shift = ortoghonal * a;
// bottom points
let a0 = p0 + shift;
let b0 = p0 - shift;
let c0 = p1 - shift;
let d0 = p1 + shift;
// top points
let a1 = a0 + h;
let b1 = b0 + h;
let c1 = c0 + h;
let d1 = d0 + h;
// Bottom
mesh.push_quad(quad(d0, c0, b0, a0));
// Faces
// (we need only two of them, because other two are inside)
mesh.push_quad(quad(d0, a0, a1, d1));
mesh.push_quad(quad(b0, c0, c1, b1));
// Top
mesh.push_quad(quad(a1, b1, c1, d1));
// 3) Draw second half-cylinder
draw_cylinder_sector(&mut mesh, p1, HALF_SECTORS, TOTAL);
},
} }
mesh mesh
} }
@ -55,7 +153,7 @@ pub struct DebugShapeId(pub u64);
pub struct Debug { pub struct Debug {
next_shape_id: DebugShapeId, next_shape_id: DebugShapeId,
pending_shapes: HashMap<DebugShapeId, DebugShape>, pending_shapes: HashMap<DebugShapeId, DebugShape>,
pending_locals: HashMap<DebugShapeId, ([f32; 4], [f32; 4])>, pending_locals: HashMap<DebugShapeId, ([f32; 4], [f32; 4], [f32; 4])>,
pending_deletes: HashSet<DebugShapeId>, pending_deletes: HashSet<DebugShapeId>,
models: HashMap<DebugShapeId, (Model<DebugVertex>, Bound<Consts<DebugLocals>>)>, models: HashMap<DebugShapeId, (Model<DebugVertex>, Bound<Consts<DebugLocals>>)>,
} }
@ -78,8 +176,8 @@ impl Debug {
id id
} }
pub fn set_pos_and_color(&mut self, id: DebugShapeId, pos: [f32; 4], color: [f32; 4]) { pub fn set_context(&mut self, id: DebugShapeId, pos: [f32; 4], color: [f32; 4], ori: [f32; 4]) {
self.pending_locals.insert(id, (pos, color)); self.pending_locals.insert(id, (pos, color, ori));
} }
pub fn remove_shape(&mut self, id: DebugShapeId) { self.pending_deletes.insert(id); } pub fn remove_shape(&mut self, id: DebugShapeId) { self.pending_deletes.insert(id); }
@ -90,6 +188,7 @@ impl Debug {
let locals = renderer.create_debug_bound_locals(&[DebugLocals { let locals = renderer.create_debug_bound_locals(&[DebugLocals {
pos: [0.0; 4], pos: [0.0; 4],
color: [1.0, 0.0, 0.0, 1.0], color: [1.0, 0.0, 0.0, 1.0],
ori: [0.0, 0.0, 0.0, 1.0],
}]); }]);
self.models.insert(id, (model, locals)); self.models.insert(id, (model, locals));
} else { } else {
@ -99,12 +198,13 @@ impl Debug {
); );
} }
} }
for (id, (pos, color)) in self.pending_locals.drain() { for (id, (pos, color, ori)) in self.pending_locals.drain() {
if let Some((_, locals)) = self.models.get_mut(&id) { if let Some((_, locals)) = self.models.get_mut(&id) {
let lc = srgba_to_linear(color.into()); let lc = srgba_to_linear(color.into());
let new_locals = [DebugLocals { let new_locals = [DebugLocals {
pos, pos,
color: [lc.r, lc.g, lc.b, lc.a], color: [lc.r, lc.g, lc.b, lc.a],
ori,
}]; }];
renderer.update_consts(locals, &new_locals); renderer.update_consts(locals, &new_locals);
} else { } else {

View File

@ -1152,32 +1152,73 @@ impl Scene {
if settings.interface.toggle_hitboxes { if settings.interface.toggle_hitboxes {
let positions = ecs.read_component::<comp::Pos>(); let positions = ecs.read_component::<comp::Pos>();
let colliders = ecs.read_component::<comp::Collider>(); let colliders = ecs.read_component::<comp::Collider>();
let orientations = ecs.read_component::<comp::Ori>();
let groups = ecs.read_component::<comp::Group>(); let groups = ecs.read_component::<comp::Group>();
for (entity, pos, collider, group) in for (entity, pos, collider, ori, group) in (
(&ecs.entities(), &positions, &colliders, groups.maybe()).join() &ecs.entities(),
&positions,
&colliders,
&orientations,
groups.maybe(),
)
.join()
{ {
if let comp::Collider::Box { match collider {
radius, comp::Collider::Box {
z_min, radius,
z_max, z_min,
} = collider z_max,
{ } => {
current_entities.insert(entity); current_entities.insert(entity);
let shape_id = hitboxes.entry(entity).or_insert_with(|| { let shape_id = hitboxes.entry(entity).or_insert_with(|| {
self.debug.add_shape(DebugShape::Cylinder { self.debug.add_shape(DebugShape::Cylinder {
radius: *radius, radius: *radius,
height: *z_max - *z_min, height: *z_max - *z_min,
}) })
}); });
let hb_pos = [pos.0.x, pos.0.y, pos.0.z + *z_min, 0.0]; let hb_pos = [pos.0.x, pos.0.y, pos.0.z + *z_min, 0.0];
let color = if group == Some(&comp::group::ENEMY) { let color = if group == Some(&comp::group::ENEMY) {
[1.0, 0.0, 0.0, 0.5] [1.0, 0.0, 0.0, 0.5]
} else if group == Some(&comp::group::NPC) { } else if group == Some(&comp::group::NPC) {
[0.0, 0.0, 1.0, 0.5] [0.0, 0.0, 1.0, 0.5]
} else { } else {
[0.0, 1.0, 0.0, 0.5] [0.0, 1.0, 0.0, 0.5]
}; };
self.debug.set_pos_and_color(*shape_id, hb_pos, color); // cylinders don't need orientation anyway
let ori = [0.0, 0.0, 0.0, 1.0];
self.debug.set_context(*shape_id, hb_pos, color, ori);
},
comp::Collider::CapsulePrism {
p0,
p1,
radius,
z_min,
z_max,
} => {
current_entities.insert(entity);
let shape_id = hitboxes.entry(entity).or_insert_with(|| {
self.debug.add_shape(DebugShape::CapsulePrism {
p0: *p0,
p1: *p1,
radius: *radius,
height: *z_max - *z_min,
})
});
let hb_pos = [pos.0.x, pos.0.y, pos.0.z + *z_min, 0.0];
let color = if group == Some(&comp::group::ENEMY) {
[1.0, 0.0, 0.0, 0.5]
} else if group == Some(&comp::group::NPC) {
[0.0, 0.0, 1.0, 0.5]
} else {
[0.0, 1.0, 0.0, 0.5]
};
let ori = ori.to_quat();
let hb_ori = [ori.x, ori.y, ori.z, ori.w];
self.debug.set_context(*shape_id, hb_pos, color, hb_ori);
},
comp::Collider::Voxel { .. } | comp::Collider::Point => {
// ignore terrain-like or point-hitboxes
},
} }
} }
} }

View File

@ -59,9 +59,10 @@ impl EguiState {
scene.debug.remove_shape(DebugShapeId(*debug_shape_id)); scene.debug.remove_shape(DebugShapeId(*debug_shape_id));
}, },
DebugShapeAction::SetPosAndColor { id, pos, color } => { DebugShapeAction::SetPosAndColor { id, pos, color } => {
let identity_ori = [0.0, 0.0, 0.0, 1.0];
scene scene
.debug .debug
.set_pos_and_color(DebugShapeId(*id), *pos, *color); .set_context(DebugShapeId(*id), *pos, *color, identity_ori);
}, },
}) })
} }