From 3b308a3f6fe9395aa2d321fd6d40bf9e704b5840 Mon Sep 17 00:00:00 2001 From: juliancoffee Date: Sat, 11 Sep 2021 15:06:13 +0300 Subject: [PATCH 01/13] Add CapsulePrism collider variant + Add placeholder physics collision implementation as copy of cylinder Box collider. + Display it with debug hitboxes. --- assets/voxygen/shaders/debug-vert.glsl | 27 ++++- common/src/comp/body.rs | 88 +++++++++++++- common/src/comp/phys.rs | 28 ++++- common/systems/src/phys.rs | 161 +++++++++++++++++-------- server/src/state_ext.rs | 26 ++++ voxygen/src/render/pipelines/debug.rs | 2 + voxygen/src/scene/debug.rs | 134 +++++++++++++++++--- voxygen/src/scene/mod.rs | 89 ++++++++++---- voxygen/src/ui/egui/mod.rs | 3 +- 9 files changed, 461 insertions(+), 97 deletions(-) diff --git a/assets/voxygen/shaders/debug-vert.glsl b/assets/voxygen/shaders/debug-vert.glsl index 97d774a642..d5f38d9b14 100644 --- a/assets/voxygen/shaders/debug-vert.glsl +++ b/assets/voxygen/shaders/debug-vert.glsl @@ -9,6 +9,7 @@ layout (std140, set = 1, binding = 0) uniform u_locals { vec4 w_pos; vec4 w_color; + vec4 w_ori; }; layout (location = 0) @@ -16,5 +17,29 @@ out vec4 f_color; void main() { 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); } diff --git a/common/src/comp/body.rs b/common/src/comp/body.rs index 6131c4c7fe..653d64b028 100644 --- a/common/src/comp/body.rs +++ b/common/src/comp/body.rs @@ -147,6 +147,20 @@ impl< 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 { 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 - /// respectively + /// respectively (in metres) + // Code reviewers: should we replace metres with 'block height'? pub fn dimensions(&self) -> Vec3 { match self { 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::Mightysaurok => Vec3::new(4.0, 3.0, 3.4), 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::Llama => Vec3::new(2.0, 2.5, 2.6), 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), }, 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 // are very much not cylindrical. Eventually this ought to be replaced by more // accurate collision shapes. @@ -394,6 +431,52 @@ impl Body { 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]: + pub fn sausage(&self) -> (Vec2, Vec2, 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 // 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 @@ -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 base_energy(&self) -> u32 { diff --git a/common/src/comp/phys.rs b/common/src/comp/phys.rs index c3512bc2cf..1a12dfce98 100644 --- a/common/src/comp/phys.rs +++ b/common/src/comp/phys.rs @@ -99,10 +99,24 @@ impl Component for Density { // Collider #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub enum Collider { - // TODO: pass the map from ids -> voxel data to get_radius and get_z_limits to compute a - // bounding cylinder - Voxel { id: String }, - Box { radius: f32, z_min: f32, z_max: f32 }, + // TODO: pass the map from ids -> voxel data to get_radius + // and get_z_limits to compute a bounding cylinder. + Voxel { + id: String, + }, + Box { + radius: f32, + z_min: f32, + z_max: f32, + }, + /// Capsule prism with line segment from p0 to p1 + CapsulePrism { + p0: Vec2, + p1: Vec2, + radius: f32, + z_min: f32, + z_max: f32, + }, Point, } @@ -111,6 +125,11 @@ impl Collider { match self { Collider::Voxel { .. } => 1.0, 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, } } @@ -124,6 +143,7 @@ impl Collider { match self { Collider::Voxel { .. } => (0.0, 1.0), 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), } } diff --git a/common/systems/src/phys.rs b/common/systems/src/phys.rs index a83ba2ddce..efefb7f245 100644 --- a/common/systems/src/phys.rs +++ b/common/systems/src/phys.rs @@ -290,8 +290,9 @@ impl<'a> PhysicsData<'a> { !&read.mountings, read.stickies.maybe(), &mut write.physics_states, - // TODO: if we need to avoid collisions for other things consider moving whether it - // should interact into the collider component or into a separate component + // TODO: if we need to avoid collisions for other things consider + // moving whether it should interact into the collider component + // or into a separate component. read.projectiles.maybe(), read.char_states.maybe(), ) @@ -322,9 +323,10 @@ impl<'a> PhysicsData<'a> { let mut entity_entity_collision_checks = 0; let mut entity_entity_collisions = 0; - // TODO: quick fix for bad performance at extrememly high velocities - // use oriented rectangles at some threshold of displacement/radius - // to query the spatial grid and limit max displacement per tick somehow + // TODO: quick fix for bad performance. At extrememly high + // velocities use oriented rectangles at some threshold of + // displacement/radius to query the spatial grid and limit + // max displacement per tick somehow. if previous_cache.collision_boundary > 128.0 { return PhysicsMetrics { entity_entity_collision_checks, @@ -419,9 +421,14 @@ impl<'a> PhysicsData<'a> { <= pos_other.z + z_limits_other.1 * previous_cache_other.scale { - // If entities have not yet collided this tick (but just - // did) and if entity is either in mid air or is not sticky, - // then mark them as colliding with the other entity + // If entities have not yet collided + // this tick (but just did) and if 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) { physics.touch_entities.insert(*other); entity_entity_collisions += 1; @@ -627,8 +634,9 @@ impl<'a> PhysicsData<'a> { // And not already stuck on a block (e.g., for arrows) && !(physics_state.on_surface().is_some() && sticky.is_some()) { - // Clamp dt to an effective 10 TPS, to prevent gravity from slamming the - // players into the floor when stationary if other systems cause the server + // Clamp dt to an effective 10 TPS, to prevent gravity + // 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). 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 outcomes = Vec::new(); - // Defer the writes of positions, velocities and orientations to allow an inner - // loop over terrain-like entities + // Defer the writes of positions, velocities and orientations + // to allow an inner loop over terrain-like entities. let old_vel = *vel; let mut vel = *vel; let old_ori = *ori; @@ -777,17 +785,23 @@ impl<'a> PhysicsData<'a> { 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 // 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 // stepping from the original position to the target for - // every obstacle, refining the target position as we go. It's not perfect, but - // it works pretty well in practice. Oddities can occur on - // the intersection between multiple 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 + // every obstacle, refining the target position as we go. + // + // It's not perfect, but it works pretty well in practice. + // Oddities can occur on the intersection between multiple + // 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. let mut tgt_pos = pos.0 + pos_delta; @@ -799,11 +813,12 @@ impl<'a> PhysicsData<'a> { match &collider { Collider::Voxel { .. } => { - // for now, treat entities with voxel colliders 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 + // For now, treat entities with voxel colliders + // 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. let radius = collider.get_radius() * scale * 0.1; let (z_min, z_max) = collider.get_z_limits(scale); @@ -832,6 +847,50 @@ impl<'a> PhysicsData<'a> { z_min, 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 let radius = radius.min(0.45) * scale; let z_min = *z_min * scale; @@ -866,8 +925,10 @@ impl<'a> PhysicsData<'a> { Collider::Point => { let mut pos = *pos; - // If the velocity is exactly 0, a raycast may not pick up the current - // block. Handle this. + // TODO: If the velocity is exactly 0, + // a raycast may not pick up the current block. + // + // Handle this. let (dist, block) = if let Some(block) = read .terrain .get(pos.0.map(|e| e.floor() as i32)) @@ -882,7 +943,8 @@ impl<'a> PhysicsData<'a> { .until(|block: &Block| block.is_solid()) .ignore_error() .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; @@ -987,6 +1049,7 @@ impl<'a> PhysicsData<'a> { // Collide with terrain-like entities let query_center = path_sphere.center.xy(); let query_radius = path_sphere.radius; + voxel_collider_spatial_grid .in_circle_aabr(query_center, query_radius) .filter_map(|entity| { @@ -1280,6 +1343,7 @@ impl<'a> System<'a> for Sys { } } +#[warn(clippy::pedantic)] #[allow(clippy::too_many_arguments)] fn box_voxel_collision<'a, T: BaseVol + ReadVol>( cylinder: (f32, f32, f32), // effective collision cylinder @@ -1297,22 +1361,6 @@ fn box_voxel_collision<'a, T: BaseVol + ReadVol>( mut land_on_ground: impl FnMut(Entity, Vel), 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 // collides with fn collision_iter<'a, T: BaseVol + ReadVol>( @@ -1357,7 +1405,6 @@ fn box_voxel_collision<'a, T: BaseVol + ReadVol>( }) } - let z_range = z_min..z_max; // Function for determining whether the player at a specific position collides // with blocks with the given criteria fn collision_with<'a, T: BaseVol + ReadVol>( @@ -1381,6 +1428,25 @@ fn box_voxel_collision<'a, T: BaseVol + ReadVol>( .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_ceiling = false; @@ -1395,7 +1461,6 @@ fn box_voxel_collision<'a, T: BaseVol + ReadVol>( .ceil() .max(1.0); let old_pos = pos.0; - fn block_true(_: &Block) -> bool { true } for _ in 0..increments as usize { pos.0 += pos_delta / increments; @@ -1485,7 +1550,7 @@ fn box_voxel_collision<'a, T: BaseVol + ReadVol>( // ...and the vertical resolution direction is sufficiently great... && dir.z < -0.1 // ...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)... // && terrain // .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 + ReadVol>( && collision_with( pos.0 + resolve_dir - Vec3::unit_z() * 1.25, &terrain, - block_true, + always_hits, near_iter.clone(), radius, z_range.clone(), @@ -1545,7 +1610,7 @@ fn box_voxel_collision<'a, T: BaseVol + ReadVol>( } else if collision_with( pos.0 - Vec3::unit_z() * 1.1, &terrain, - block_true, + always_hits, near_iter.clone(), radius, z_range.clone(), diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index d51463ae7e..78009fa908 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -185,6 +185,7 @@ impl StateExt for State { inventory: comp::Inventory, body: comp::Body, ) -> EcsEntityBuilder { + use comp::body::{biped_large, quadruped_medium}; self.ecs_mut() .create_entity_synced() .with(pos) @@ -203,6 +204,31 @@ impl StateExt for State { comp::Body::Ship(ship) => comp::Collider::Voxel { 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 { radius: body.radius(), z_min: 0.0, diff --git a/voxygen/src/render/pipelines/debug.rs b/voxygen/src/render/pipelines/debug.rs index e096510dc4..f35f49b320 100644 --- a/voxygen/src/render/pipelines/debug.rs +++ b/voxygen/src/render/pipelines/debug.rs @@ -36,6 +36,8 @@ pub struct Locals { /// by the shader pub pos: [f32; 4], pub color: [f32; 4], + /// quaternion as [x, y, z, w] + pub ori: [f32; 4], } pub type BoundLocals = Bound>; diff --git a/voxygen/src/scene/debug.rs b/voxygen/src/scene/debug.rs index bbe2832d34..b4fbf2d0d4 100644 --- a/voxygen/src/scene/debug.rs +++ b/voxygen/src/scene/debug.rs @@ -9,7 +9,16 @@ use vek::*; #[derive(Debug)] pub enum DebugShape { Line([Vec3; 2]), - Cylinder { radius: f32, height: f32 }, + Cylinder { + radius: f32, + height: f32, + }, + CapsulePrism { + p0: Vec2, + p1: Vec2, + radius: f32, + height: f32, + }, } impl DebugShape { @@ -22,28 +31,117 @@ impl DebugShape { let quad = |x: Vec3, y: Vec3, z: Vec3, w: Vec3| { Quad::::new(x.into(), y.into(), z.into(), w.into()) }; + match self { DebugShape::Line([a, b]) => { let h = Vec3::new(0.0, 1.0, 0.0); mesh.push_quad(quad(*a, a + h, b + h, *b)); }, DebugShape::Cylinder { radius, height } => { - const SUBDIVISIONS: usize = 16; + const SUBDIVISIONS: u8 = 16; for i in 0..SUBDIVISIONS { - let angle = |j: usize| (j as f32 / SUBDIVISIONS as f32) * 2.0 * PI; - let a = Vec3::zero(); - let b = Vec3::new(radius * angle(i).cos(), radius * angle(i).sin(), 0.0); - let c = Vec3::new( - radius * angle(i + 1).cos(), - radius * angle(i + 1).sin(), - 0.0, - ); + // dot on circle edge + let to = |n: u8| { + const FULL: f32 = 2.0 * PI; + let angle = FULL * f32::from(n) / f32::from(SUBDIVISIONS); + + Vec3::new(radius * angle.cos(), radius * angle.sin(), 0.0) + }; + + let origin = Vec3::zero(); + let r0 = to(i); + let r1 = to(i + 1); + 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)); - mesh.push_tri(tri(a + h, b + h, c + h)); + + // 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)); } }, + 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, origin: Vec3, 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 } @@ -55,7 +153,7 @@ pub struct DebugShapeId(pub u64); pub struct Debug { next_shape_id: DebugShapeId, pending_shapes: HashMap, - pending_locals: HashMap, + pending_locals: HashMap, pending_deletes: HashSet, models: HashMap, Bound>)>, } @@ -78,8 +176,8 @@ impl Debug { id } - pub fn set_pos_and_color(&mut self, id: DebugShapeId, pos: [f32; 4], color: [f32; 4]) { - self.pending_locals.insert(id, (pos, color)); + pub fn set_context(&mut self, id: DebugShapeId, pos: [f32; 4], color: [f32; 4], ori: [f32; 4]) { + self.pending_locals.insert(id, (pos, color, ori)); } 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 { pos: [0.0; 4], color: [1.0, 0.0, 0.0, 1.0], + ori: [0.0, 0.0, 0.0, 1.0], }]); self.models.insert(id, (model, locals)); } 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) { let lc = srgba_to_linear(color.into()); let new_locals = [DebugLocals { pos, color: [lc.r, lc.g, lc.b, lc.a], + ori, }]; renderer.update_consts(locals, &new_locals); } else { diff --git a/voxygen/src/scene/mod.rs b/voxygen/src/scene/mod.rs index 97399de4bb..8d41883e28 100644 --- a/voxygen/src/scene/mod.rs +++ b/voxygen/src/scene/mod.rs @@ -1152,32 +1152,73 @@ impl Scene { if settings.interface.toggle_hitboxes { let positions = ecs.read_component::(); let colliders = ecs.read_component::(); + let orientations = ecs.read_component::(); let groups = ecs.read_component::(); - for (entity, pos, collider, group) in - (&ecs.entities(), &positions, &colliders, groups.maybe()).join() + for (entity, pos, collider, ori, group) in ( + &ecs.entities(), + &positions, + &colliders, + &orientations, + groups.maybe(), + ) + .join() { - if let comp::Collider::Box { - radius, - z_min, - z_max, - } = collider - { - current_entities.insert(entity); - let shape_id = hitboxes.entry(entity).or_insert_with(|| { - self.debug.add_shape(DebugShape::Cylinder { - 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] - }; - self.debug.set_pos_and_color(*shape_id, hb_pos, color); + match collider { + comp::Collider::Box { + radius, + z_min, + z_max, + } => { + current_entities.insert(entity); + let shape_id = hitboxes.entry(entity).or_insert_with(|| { + self.debug.add_shape(DebugShape::Cylinder { + 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] + }; + // 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 + }, } } } diff --git a/voxygen/src/ui/egui/mod.rs b/voxygen/src/ui/egui/mod.rs index d3b7333fcf..47dab5aa33 100644 --- a/voxygen/src/ui/egui/mod.rs +++ b/voxygen/src/ui/egui/mod.rs @@ -59,9 +59,10 @@ impl EguiState { scene.debug.remove_shape(DebugShapeId(*debug_shape_id)); }, DebugShapeAction::SetPosAndColor { id, pos, color } => { + let identity_ori = [0.0, 0.0, 0.0, 1.0]; scene .debug - .set_pos_and_color(DebugShapeId(*id), *pos, *color); + .set_context(DebugShapeId(*id), *pos, *color, identity_ori); }, }) } From 7712976b86d6bccbe19e4593ab0367409f3e4d02 Mon Sep 17 00:00:00 2001 From: juliancoffee Date: Tue, 14 Sep 2021 23:04:55 +0300 Subject: [PATCH 02/13] Refactor box_voxel_collision function --- common/systems/src/phys.rs | 251 +++++++++++++++++++++---------------- 1 file changed, 143 insertions(+), 108 deletions(-) diff --git a/common/systems/src/phys.rs b/common/systems/src/phys.rs index efefb7f245..10b9113138 100644 --- a/common/systems/src/phys.rs +++ b/common/systems/src/phys.rs @@ -1344,7 +1344,7 @@ impl<'a> System<'a> for Sys { } #[warn(clippy::pedantic)] -#[allow(clippy::too_many_arguments)] +#[allow(clippy::too_many_arguments, clippy::too_many_lines)] fn box_voxel_collision<'a, T: BaseVol + ReadVol>( cylinder: (f32, f32, f32), // effective collision cylinder terrain: &'a T, @@ -1361,6 +1361,13 @@ fn box_voxel_collision<'a, T: BaseVol + ReadVol>( mut land_on_ground: impl FnMut(Entity, Vel), read: &PhysicsRead, ) { + // FIXME: Review these + #![allow( + clippy::cast_precision_loss, + clippy::cast_possible_truncation, + clippy::cast_sign_loss + )] + // Function for iterating over the blocks the player at a specific position // collides with fn collision_iter<'a, T: BaseVol + ReadVol>( @@ -1375,9 +1382,10 @@ fn box_voxel_collision<'a, T: BaseVol + ReadVol>( near_iter.filter_map(move |(i, j, k)| { let block_pos = pos.map(|e| e.floor() as i32) + Vec3::new(i, j, k); - // `near_iter` could be a few blocks too large due to being integer aligned and - // rounding up, so skip points outside of the tighter bounds before looking them - // up in the terrain (which incurs a hashmap cost for volgrids) + // `near_iter` could be a few blocks too large due to being integer + // aligned and rounding up, so skip points outside of the tighter + // bounds before looking them up in the terrain + // (which incurs a hashmap cost for volgrids) let player_aabb = Aabb { min: pos + Vec3::new(-radius, -radius, z_range.start), max: pos + Vec3::new(radius, radius, z_range.end), @@ -1428,21 +1436,27 @@ fn box_voxel_collision<'a, T: BaseVol + ReadVol>( .is_some() } + // Should be easy to just make clippy happy if we want? + #[allow(clippy::trivially_copy_pass_by_ref)] 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)) + let near_iter = (-hdist..=hdist) + .flat_map(move |i| { + (-hdist..=hdist).map(move |j| { + let max_block_height = Block::MAX_HEIGHT.ceil() as i32; + let box_floor = z_min.floor() as i32; + let floor = 1 - max_block_height + box_floor; + let ceil = z_max.ceil() as i32; + + (floor..=ceil).map(move |k| (i, j, k)) }) }) - .flatten() .flatten(); let z_range = z_min..z_max; @@ -1452,7 +1466,8 @@ fn box_voxel_collision<'a, T: BaseVol + ReadVol>( let mut on_ground = None; let mut on_ceiling = false; - let mut attempts = 0; // Don't loop infinitely here + // Don't loop infinitely here + let mut attempts = 0; let mut pos_delta = tgt_pos - pos.0; @@ -1461,54 +1476,62 @@ fn box_voxel_collision<'a, T: BaseVol + ReadVol>( .ceil() .max(1.0); let old_pos = pos.0; + for _ in 0..increments as usize { + const MAX_ATTEMPTS: usize = 16; pos.0 += pos_delta / increments; - const MAX_ATTEMPTS: usize = 16; + let try_colliding_block = |pos: &Pos| { + // Calculate the player's AABB + let player_aabb = Aabb { + min: pos.0 + Vec3::new(-radius, -radius, z_min), + max: pos.0 + Vec3::new(radius, radius, z_max), + }; + + // Determine the block that we are colliding with most + // (based on minimum collision axis) + // (if we are colliding with one) + // + // 1) Calculate the block's positions in world space + // 2) Make sure the block is actually solid + // 3) Calculate block AABB + // 4) Find the maximum of the minimum collision axes + // (this bit is weird, trust me that it works) + near_iter + .clone() + .map(|(i, j, k)| pos.0.map(|e| e.floor() as i32) + Vec3::new(i, j, k)) + .filter_map(|block_pos| { + terrain + .get(block_pos) + .ok() + .filter(|block| block.is_solid()) + .map(|block| (block_pos, block)) + }) + .map(|(block_pos, block)| { + ( + block_pos, + Aabb { + min: block_pos.map(|e| e as f32), + max: block_pos.map(|e| e as f32) + + Vec3::new(1.0, 1.0, block.solid_height()), + }, + block, + ) + }) + .filter(|(_, block_aabb, _)| block_aabb.collides_with_aabb(player_aabb)) + .min_by_key(|(_, block_aabb, _)| { + ordered_float::OrderedFloat( + (block_aabb.center() - player_aabb.center() - Vec3::unit_z() * 0.5) + .map(f32::abs) + .sum(), + ) + }) + }; // While the player is colliding with the terrain... - while let Some((_block_pos, block_aabb, block)) = - (attempts < MAX_ATTEMPTS).then(|| { - // Calculate the player's AABB - let player_aabb = Aabb { - min: pos.0 + Vec3::new(-radius, -radius, z_min), - max: pos.0 + Vec3::new(radius, radius, z_max), - }; - - // Determine the block that we are colliding with most (based on minimum - // collision axis) (if we are colliding with one) - near_iter - .clone() - // Calculate the block's position in world space - .map(|(i, j, k)| pos.0.map(|e| e.floor() as i32) + Vec3::new(i, j, k)) - // Make sure the block is actually solid - .filter_map(|block_pos| { - terrain - .get(block_pos) - .ok() - .filter(|block| block.is_solid()) - .map(|block| (block_pos, block)) - }) - // Calculate block AABB - .map(|(block_pos, block)| { - ( - block_pos, - Aabb { - min: block_pos.map(|e| e as f32), - max: block_pos.map(|e| e as f32) + Vec3::new(1.0, 1.0, block.solid_height()), - }, - block, - ) - }) - // Determine whether the block's AABB collides with the player's AABB - .filter(|(_, block_aabb, _)| block_aabb.collides_with_aabb(player_aabb)) - // Find the maximum of the minimum collision axes (this bit is weird, trust me that it works) - .min_by_key(|(_, block_aabb, _)| { - ordered_float::OrderedFloat((block_aabb.center() - player_aabb.center() - Vec3::unit_z() * 0.5) - .map(f32::abs) - .sum()) - }) - }).flatten() + while let Some((_block_pos, block_aabb, block)) = (attempts < MAX_ATTEMPTS) + .then(|| try_colliding_block(pos)) + .flatten() { // Calculate the player's AABB let player_aabb = Aabb { @@ -1532,10 +1555,9 @@ fn box_voxel_collision<'a, T: BaseVol + ReadVol>( // When the resolution direction is pointing upwards, we must be on the // ground - if resolve_dir.z > 0.0 - /* && vel.0.z <= 0.0 */ - { - on_ground = Some(block).copied(); + /* if resolve_dir.z > 0.0 && vel.0.z <= 0.0 { */ + if resolve_dir.z > 0.0 { + on_ground = Some(*block); if !was_on_ground { land_on_ground(entity, *vel); @@ -1545,48 +1567,58 @@ fn box_voxel_collision<'a, T: BaseVol + ReadVol>( } // When the resolution direction is non-vertical, we must be colliding - // with a wall If we're being pushed out horizontally... - if resolve_dir.z == 0.0 - // ...and the vertical resolution direction is sufficiently great... - && dir.z < -0.1 - // ...and the space above is free... - && !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)... - // && terrain - // .get((pos.0 - Vec3::unit_z() * 0.1).map(|e| e.floor() as i32)) - // .map(|block| block.is_solid()) - // .unwrap_or(false) - // ...and there is a collision with a block beneath our current hitbox... - && collision_with( - pos.0 + resolve_dir - Vec3::unit_z() * 1.25, - &terrain, - always_hits, - near_iter.clone(), - radius, - z_range.clone(), - ) + // with a wall + // + // If we're being pushed out horizontally... + let pushed_horizontaly = resolve_dir.z == 0.0; + // ...and the vertical resolution direction is sufficiently great... + let vertical_resolution = dir.z < -0.1; + // ...and the space above is free... + let space_above_is_free = !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 there is a collision with a block beneath our current hitbox... + let block_beneath_collides = collision_with( + pos.0 + resolve_dir - Vec3::unit_z() * 1.25, + &terrain, + always_hits, + near_iter.clone(), + radius, + z_range.clone(), + ); + + if pushed_horizontaly + && vertical_resolution + && space_above_is_free + && block_beneath_collides { // ...block-hop! pos.0.z = pos.0.z.max(block_aabb.max.z); vel.0.z = vel.0.z.max(0.0); - // Push the character on to the block very slightly to avoid jitter due to imprecision - if (vel.0 * resolve_dir).xy().magnitude_squared() < 1.0f32.powi(2) { + // Push the character on to the block very slightly + // to avoid jitter due to imprecision + if (vel.0 * resolve_dir).xy().magnitude_squared() < 1.0_f32.powi(2) { pos.0 -= resolve_dir.normalized() * 0.05; } - on_ground = Some(block).copied(); + on_ground = Some(*block); break; - } else { - // Correct the velocity - vel.0 = vel.0.map2( - resolve_dir, - |e, d| { - if d * e.signum() < 0.0 { 0.0 } else { e } - }, - ); - - pos_delta *= resolve_dir.map(|e| if e != 0.0 { 0.0 } else { 1.0 }); } + // If not, correct the velocity + vel.0 = vel.0.map2( + resolve_dir, + |e, d| { + if d * e.signum() < 0.0 { 0.0 } else { e } + }, + ); + + pos_delta *= resolve_dir.map(|e| if e == 0.0 { 1.0 } else { 0.0 }); + // Resolve the collision normally pos.0 += resolve_dir; @@ -1622,8 +1654,7 @@ fn box_voxel_collision<'a, T: BaseVol + ReadVol>( .get(Vec3::new(pos.0.x, pos.0.y, pos.0.z - 0.1).map(|e| e.floor() as i32)) .ok() .filter(|block| block.is_solid()) - .map(|block| block.solid_height()) - .unwrap_or(0.0); + .map_or(0.0, Block::solid_height); vel.0.z = 0.0; pos.0.z = (pos.0.z - 0.1).floor() + snap_height; physics_state.on_ground = terrain @@ -1702,28 +1733,32 @@ fn box_voxel_collision<'a, T: BaseVol + ReadVol>( }); } } + physics_state.on_wall = on_wall; let fric_mod = read.stats.get(entity).map_or(1.0, |s| s.friction_modifier); + if physics_state.on_ground.is_some() || (physics_state.on_wall.is_some() && climbing) { vel.0 *= (1.0 - FRIC_GROUND.min(1.0) * fric_mod).powf(dt.0 * 60.0); physics_state.ground_vel = ground_vel; } physics_state.in_fluid = liquid - .map(|(kind, max_z)| (kind, max_z - pos.0.z)) // NOTE: assumes min_z == 0.0 - .map(|(kind, depth)| { - (kind, physics_state - .in_liquid() - // This is suboptimal because it doesn't check for true depth, - // so it can cause problems for situations like swimming down - // a river and spawning or teleporting in(/to) water - .map(|old_depth| (old_depth + old_pos.z - pos.0.z).max(depth)) - .unwrap_or(depth)) - }) - .map(|(kind, depth)| Fluid::Liquid { - kind, - depth, - vel: Vel::zero(), + .map(|(kind, max_z)| { + // NOTE: assumes min_z == 0.0 + let depth = max_z - pos.0.z; + + // This is suboptimal because it doesn't check for true depth, + // so it can cause problems for situations like swimming down + // a river and spawning or teleporting in(/to) water + let new_depth = physics_state.in_liquid().map_or(depth, |old_depth| { + (old_depth + old_pos.z - pos.0.z).max(depth) + }); + + Fluid::Liquid { + kind, + depth: new_depth, + vel: Vel::zero(), + } }) .or_else(|| match physics_state.in_fluid { Some(Fluid::Liquid { .. }) | None => Some(Fluid::Air { From 6c3b61dc25efa28b8a4bbf58b9997ba0f5408864 Mon Sep 17 00:00:00 2001 From: juliancoffee Date: Wed, 15 Sep 2021 18:11:41 +0300 Subject: [PATCH 03/13] Implement CapsulePrism collisions --- common/src/comp/phys.rs | 12 +- common/systems/src/phys.rs | 280 ++++++++++++++++++++++++------------- 2 files changed, 193 insertions(+), 99 deletions(-) diff --git a/common/src/comp/phys.rs b/common/src/comp/phys.rs index 1a12dfce98..8364ccba29 100644 --- a/common/src/comp/phys.rs +++ b/common/src/comp/phys.rs @@ -55,6 +55,9 @@ pub struct PreviousPhysCache { pub collision_boundary: f32, pub scale: f32, pub scaled_radius: f32, + pub neighborhood_radius: f32, + /// p0 and p1 in case of CapsulePrism collider, Vec2::zero() otherwise. + pub origins: (Vec2, Vec2), pub ori: Quaternion, } @@ -125,11 +128,10 @@ impl Collider { match self { Collider::Voxel { .. } => 1.0, 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::CapsulePrism { radius, p0, p1, .. } => { + let a = p0.distance(*p1); + a / 2.0 + *radius + }, Collider::Point => 0.0, } } diff --git a/common/systems/src/phys.rs b/common/systems/src/phys.rs index 10b9113138..0b91aaadab 100644 --- a/common/systems/src/phys.rs +++ b/common/systems/src/phys.rs @@ -192,15 +192,18 @@ impl<'a> PhysicsData<'a> { collision_boundary: 0.0, scale: 0.0, scaled_radius: 0.0, + neighborhood_radius: 0.0, + origins: (Vec2::zero(), Vec2::zero()), ori: Quaternion::identity(), }); } // Update PreviousPhysCache - for (_, vel, position, mut phys_cache, collider, scale, cs, _, _, _) in ( + for (_, vel, position, ori, mut phys_cache, collider, scale, cs, _, _, _) in ( &self.read.entities, &self.write.velocities, &self.write.positions, + &self.write.orientations, &mut self.write.previous_phys_cache, self.read.colliders.maybe(), self.read.scales.maybe(), @@ -219,6 +222,10 @@ impl<'a> PhysicsData<'a> { phys_cache.velocity_dt = vel.0 * self.read.dt.0; let entity_center = position.0 + Vec3::new(0.0, z_limits.0 + half_height, 0.0); let flat_radius = collider.map(|c| c.get_radius()).unwrap_or(0.5) * scale; + let neighborhood_radius = match collider { + Some(Collider::CapsulePrism { radius, .. }) => radius * scale, + _ => flat_radius, + }; let radius = (flat_radius.powi(2) + half_height.powi(2)).sqrt(); // Move center to the middle between OLD and OLD+VEL_DT so that we can reduce @@ -226,7 +233,36 @@ impl<'a> PhysicsData<'a> { phys_cache.center = entity_center + phys_cache.velocity_dt / 2.0; phys_cache.collision_boundary = radius + (phys_cache.velocity_dt / 2.0).magnitude(); phys_cache.scale = scale; + phys_cache.neighborhood_radius = neighborhood_radius; phys_cache.scaled_radius = flat_radius; + + let ori = ori.to_quat(); + let (p0, p1) = match collider { + Some(Collider::CapsulePrism { p0, p1, .. }) => { + // Build the line between two origins + let a = p1 - p0; + let len = a.magnitude(); + // Make it 3d + let a = Vec3::new(a.x, a.y, 0.0); + // Rotate it + let a = ori * a; + // Make it 2d again + let a = Vec2::new(a.x, a.y); + // Make sure we have the same length as before + // (and scale it) + let a = a * scale * len / a.magnitude(); + + // Splite the oriented line into two origins + let p0 = -a / 2.0; + let p1 = a / 2.0; + (p0, p1) + }, + Some(Collider::Box { .. } | Collider::Voxel { .. } | Collider::Point) | None => { + (Vec2::zero(), Vec2::zero()) + }, + }; + phys_cache.origins = (p0, p1); + phys_cache.ori = ori; } } @@ -349,22 +385,20 @@ impl<'a> PhysicsData<'a> { spatial_grid .in_circle_aabr(query_center, query_radius) .filter_map(|entity| { - read.uids - .get(entity) - .and_then(|l| positions.get(entity).map(|r| (l, r))) - .and_then(|l| previous_phys_cache.get(entity).map(|r| (l, r))) - .and_then(|l| read.masses.get(entity).map(|r| (l, r))) - .map(|(((uid, pos), previous_cache), mass)| { - ( - entity, - uid, - pos, - previous_cache, - mass, - read.colliders.get(entity), - read.char_states.get(entity), - ) - }) + let uid = read.uids.get(entity)?; + let pos = positions.get(entity)?; + let previous_cache = previous_phys_cache.get(entity)?; + let mass = read.masses.get(entity)?; + + Some(( + entity, + uid, + pos, + previous_cache, + mass, + read.colliders.get(entity), + read.char_states.get(entity), + )) }) .for_each( |( @@ -387,8 +421,6 @@ impl<'a> PhysicsData<'a> { return; } - let collision_dist = previous_cache.scaled_radius - + previous_cache_other.scaled_radius; let z_limits_other = calc_z_limit(char_state_other_maybe, collider_other); @@ -403,87 +435,121 @@ impl<'a> PhysicsData<'a> { .ceil() as usize; let step_delta = 1.0 / increments as f32; + let mut collision_registered = false; + let collision_dist = previous_cache.neighborhood_radius + + previous_cache_other.neighborhood_radius; + for i in 0..increments { let factor = i as f32 * step_delta; + + // get positions let pos = pos.0 + previous_cache.velocity_dt * factor; + let pos_other = pos_other.0 + previous_cache_other.velocity_dt * factor; - let diff = pos.xy() - pos_other.xy(); + // Compare Z ranges + let ceiling = pos.z + z_limits.1 * previous_cache.scale; + let floor = pos.z + z_limits.0 * previous_cache.scale; - if diff.magnitude_squared() <= collision_dist.powi(2) - && pos.z + z_limits.1 * previous_cache.scale - >= pos_other.z - + z_limits_other.0 * previous_cache_other.scale - && pos.z + z_limits.0 * previous_cache.scale - <= pos_other.z - + z_limits_other.1 * previous_cache_other.scale - { - // If entities have not yet collided - // this tick (but just did) and if 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) { - physics.touch_entities.insert(*other); - entity_entity_collisions += 1; - } + let ceiling_other = + pos_other.z + z_limits_other.1 * previous_cache_other.scale; + let floor_other = + pos_other.z + z_limits_other.0 * previous_cache_other.scale; + let in_z_range = + ceiling >= floor_other && floor <= ceiling_other; - // Don't apply e2e pushback to entities - // that are in a forced movement state - // (e.g. roll, leapmelee). - // - // This allows leaps to work properly - // (since you won't get pushed away before - // delivering the hit), and allows - // rolling through an enemy when trapped - // (e.g. with minotaur). - // - // This allows using e2e pushback to - // gain speed by jumping out of a roll - // while in the middle of a collider, this - // is an intentional combat mechanic. - let forced_movement = matches!( - char_state_maybe, - Some(cs) if cs.is_forced_movement()); + // check horizontal distance + let (p0_offset, p1_offset) = previous_cache.origins; + let p0 = pos + p0_offset; + let p1 = pos + p1_offset; + let segment = LineSegment2 { + start: p0.xy(), + end: p1.xy(), + }; - // Don't apply repulsive force - // to projectiles - // - // or if we're colliding with a - // terrain-like entity, - // - // or if we are a terrain-like entity - // - // Don't apply force when entity - // is a sticky which is on the - // ground (or on the wall) - if !forced_movement - && (!is_sticky || is_mid_air) - && diff.magnitude_squared() > 0.0 - && !is_projectile - && !matches!( - collider_other, - Some(Collider::Voxel { .. }) - ) - && !matches!(collider, Some(Collider::Voxel { .. })) - { - let force = 400.0 - * (collision_dist - diff.magnitude()) - * mass_other.0 - / (mass.0 + mass_other.0); + let (p0_offset_other, p1_offset_other) = + previous_cache_other.origins; + let p0_other = pos_other + p0_offset_other; + let p1_other = pos_other + p1_offset_other; - vel_delta += - Vec3::from(diff.normalized()) * force * step_delta; - } + let segment_other = LineSegment2 { + start: p0_other.xy(), + end: p1_other.xy(), + }; - collision_registered = true; + let diff = projection_between(segment, segment_other); + + let in_collision_range = + diff.magnitude_squared() <= collision_dist.powi(2); + + let collides = in_collision_range && in_z_range; + + if !collides { + continue; } + + // If entities have not yet collided + // this tick (but just did) and if 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) { + physics.touch_entities.insert(*other); + entity_entity_collisions += 1; + } + + // Don't apply e2e pushback to entities + // that are in a forced movement state + // (e.g. roll, leapmelee). + // + // This allows leaps to work properly + // (since you won't get pushed away before + // delivering the hit), and allows + // rolling through an enemy when trapped + // (e.g. with minotaur). + // + // This allows using e2e pushback to + // gain speed by jumping out of a roll + // while in the middle of a collider, this + // is an intentional combat mechanic. + let forced_movement = matches!( + char_state_maybe, + Some(cs) if cs.is_forced_movement()); + + // Don't apply repulsive force + // to projectiles + // + // or if we're colliding with a + // terrain-like entity, + // + // or if we are a terrain-like entity + // + // Don't apply force when entity + // is a sticky which is on the + // ground (or on the wall) + if !forced_movement + && (!is_sticky || is_mid_air) + && diff.magnitude_squared() > 0.0 + && !is_projectile + && !matches!(collider_other, Some(Collider::Voxel { .. })) + && !matches!(collider, Some(Collider::Voxel { .. })) + { + let force = 400.0 + * (collision_dist - diff.magnitude()) + * mass_other.0 + / (mass.0 + mass_other.0); + + vel_delta += + Vec3::from(diff.normalized()) * force * step_delta; + } + + collision_registered = true; } }, ); @@ -848,7 +914,7 @@ impl<'a> PhysicsData<'a> { z_max, } => { // FIXME: - // !! DEPRECATED !! + // Deprecated, should remove? // // Scale collider let radius = radius.min(0.45) * scale; @@ -882,17 +948,14 @@ impl<'a> PhysicsData<'a> { tgt_pos = cpos.0; }, Collider::CapsulePrism { - radius, z_min, z_max, - .. + p0: _, + p1: _, + radius: _, } => { - // FIXME: placeholder. - // copypasted from Box collider - // - // This shoudln't pass Code Review. // Scale collider - let radius = radius.min(0.45) * scale; + let radius = collider.get_radius().min(0.45) * scale; let z_min = *z_min * scale; let z_max = z_max.clamped(1.2, 1.95) * scale; @@ -1798,3 +1861,32 @@ fn voxel_collider_bounding_sphere( radius, } } + +/// Get the shortest line segment between AB and CD, used for pushback +/// and collision checks. +fn projection_between(ab: LineSegment2, cd: LineSegment2) -> Vec2 { + // On 2d we can just check projection of A to CD, B to CD, C to AB and D to AB. + // + // NOTE: We don't check if segments are intersecting, because + // even if they do, we still need to return pushback vector. + let a = ab.start; + let b = ab.end; + let c = cd.start; + let d = cd.end; + + let projections = [ + // A to CD + a - cd.projected_point(a), + // B to CD + b - cd.projected_point(b), + // C to AB + c - ab.projected_point(c), + // D to AB + d - ab.projected_point(d), + ]; + + // min_by_key returns None only if iterator is empty, so unwrap is fine + IntoIterator::into_iter(projections) + .min_by_key(|p| ordered_float::OrderedFloat(p.magnitude_squared())) + .unwrap() +} From d86c9f2678303fabbd29ffed656fd35c78b9b883 Mon Sep 17 00:00:00 2001 From: juliancoffee Date: Wed, 15 Sep 2021 18:33:16 +0300 Subject: [PATCH 04/13] Remove body::Shape enum, make npc use CapsulePrism --- common/src/comp/body.rs | 57 ++++++++++---------------------------- server/src/state_ext.rs | 21 ++------------ voxygen/src/scene/debug.rs | 1 + 3 files changed, 17 insertions(+), 62 deletions(-) diff --git a/common/src/comp/body.rs b/common/src/comp/body.rs index 653d64b028..acc747b49c 100644 --- a/common/src/comp/body.rs +++ b/common/src/comp/body.rs @@ -154,12 +154,6 @@ impl< // 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 { pub fn is_humanoid(&self) -> bool { matches!(self, Body::Humanoid(_)) } @@ -403,26 +397,6 @@ 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 // are very much not cylindrical. Eventually this ought to be replaced by more // accurate collision shapes. @@ -451,29 +425,26 @@ impl Body { // 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; + if length > width { + // Dachshund-like + let radius = width / 2.0; - let a = length - 2.0 * radius; - debug_assert!(a > 0.0); + let a = length - 2.0 * radius; - let p0 = Vec2::new(0.0, -a / 2.0); - let p1 = Vec2::new(0.0, a / 2.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; + (p0, p1, radius) + } else { + // Cyclops-like + let radius = length / 2.0; - let a = width - 2.0 * radius; - debug_assert!(a > 0.0); + let a = width - 2.0 * radius; - let p0 = Vec2::new(-a / 2.0, 0.0); - let p1 = Vec2::new(a / 2.0, 0.0); + let p0 = Vec2::new(-a / 2.0, 0.0); + let p1 = Vec2::new(a / 2.0, 0.0); - (p0, p1, radius) - }, + (p0, p1, radius) } } diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index 78009fa908..c7e40f42dc 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -185,7 +185,6 @@ impl StateExt for State { inventory: comp::Inventory, body: comp::Body, ) -> EcsEntityBuilder { - use comp::body::{biped_large, quadruped_medium}; self.ecs_mut() .create_entity_synced() .with(pos) @@ -204,18 +203,7 @@ impl StateExt for State { comp::Body::Ship(ship) => comp::Collider::Voxel { 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: @@ -228,12 +216,7 @@ impl StateExt for State { z_min: 0.0, z_max: body.height(), } - }, - _ => comp::Collider::Box { - radius: body.radius(), - z_min: 0.0, - z_max: body.height(), - }, + } }) .with(comp::Controller::default()) .with(body) diff --git a/voxygen/src/scene/debug.rs b/voxygen/src/scene/debug.rs index b4fbf2d0d4..f9042615b9 100644 --- a/voxygen/src/scene/debug.rs +++ b/voxygen/src/scene/debug.rs @@ -153,6 +153,7 @@ pub struct DebugShapeId(pub u64); pub struct Debug { next_shape_id: DebugShapeId, pending_shapes: HashMap, + #[allow(clippy::type_complexity)] pending_locals: HashMap, pending_deletes: HashSet, models: HashMap, Bound>)>, From 44962958d876ef634719c9c3b1af90e3d00b05aa Mon Sep 17 00:00:00 2001 From: juliancoffee Date: Wed, 15 Sep 2021 20:48:42 +0300 Subject: [PATCH 05/13] Adress feedback - Rewrite 2 * PI to TAU - Some comment formatting --- common/systems/src/phys.rs | 1 - voxygen/src/scene/debug.rs | 8 +++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/common/systems/src/phys.rs b/common/systems/src/phys.rs index 0b91aaadab..0a941be274 100644 --- a/common/systems/src/phys.rs +++ b/common/systems/src/phys.rs @@ -866,7 +866,6 @@ impl<'a> PhysicsData<'a> { // Oddities can occur on the intersection between multiple // 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. let mut tgt_pos = pos.0 + pos_delta; diff --git a/voxygen/src/scene/debug.rs b/voxygen/src/scene/debug.rs index f9042615b9..747e80c364 100644 --- a/voxygen/src/scene/debug.rs +++ b/voxygen/src/scene/debug.rs @@ -23,7 +23,7 @@ pub enum DebugShape { impl DebugShape { pub fn mesh(&self) -> Mesh { - use core::f32::consts::PI; + use core::f32::consts::{PI, TAU}; let mut mesh = Mesh::new(); let tri = |x: Vec3, y: Vec3, z: Vec3| { Tri::::new(x.into(), y.into(), z.into()) @@ -42,8 +42,7 @@ impl DebugShape { for i in 0..SUBDIVISIONS { // dot on circle edge let to = |n: u8| { - const FULL: f32 = 2.0 * PI; - let angle = FULL * f32::from(n) / f32::from(SUBDIVISIONS); + let angle = TAU * f32::from(n) / f32::from(SUBDIVISIONS); Vec3::new(radius * angle.cos(), radius * angle.sin(), 0.0) }; @@ -80,8 +79,7 @@ impl DebugShape { 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 angle = offset + TAU * f32::from(n) / f32::from(TOTAL); let (x, y) = (radius * angle.cos(), radius * angle.sin()); let to_edge = Vec3::new(x, y, 0.0); From eeb3bec8ad5c5dd3946b2a7e0b73a4c759ae3888 Mon Sep 17 00:00:00 2001 From: juliancoffee Date: Thu, 16 Sep 2021 00:27:48 +0300 Subject: [PATCH 06/13] Refactor implementation of e2e collision + Add some hopefully helpful comments + Extract colliding tries to separate function + Move to Capsule + Cylinder collider combination instead of Capsule + Capsule. --- common/src/comp/phys.rs | 6 +- common/systems/src/phys.rs | 353 ++++++++++++++++++++++--------------- server/src/state_ext.rs | 2 +- 3 files changed, 212 insertions(+), 149 deletions(-) diff --git a/common/src/comp/phys.rs b/common/src/comp/phys.rs index 8364ccba29..1c3c73b698 100644 --- a/common/src/comp/phys.rs +++ b/common/src/comp/phys.rs @@ -54,10 +54,12 @@ pub struct PreviousPhysCache { /// Calculates a Sphere over the Entity for quick boundary checking pub collision_boundary: f32, pub scale: f32, + /// Approximate radius of cylinder of collider. pub scaled_radius: f32, + /// Radius of stadium of collider. pub neighborhood_radius: f32, - /// p0 and p1 in case of CapsulePrism collider, Vec2::zero() otherwise. - pub origins: (Vec2, Vec2), + /// relative p0 and p1 of collider's statium, None if cylinder. + pub origins: Option<(Vec2, Vec2)>, pub ori: Quaternion, } diff --git a/common/systems/src/phys.rs b/common/systems/src/phys.rs index 0a941be274..76325ddb15 100644 --- a/common/systems/src/phys.rs +++ b/common/systems/src/phys.rs @@ -23,7 +23,7 @@ use specs::{ Entities, Entity, Join, ParJoin, Read, ReadExpect, ReadStorage, SystemData, Write, WriteExpect, WriteStorage, }; -use std::ops::Range; +use std::ops::{ControlFlow, Range}; use vek::*; /// The density of the fluid as a function of submersion ratio in given fluid @@ -193,7 +193,7 @@ impl<'a> PhysicsData<'a> { scale: 0.0, scaled_radius: 0.0, neighborhood_radius: 0.0, - origins: (Vec2::zero(), Vec2::zero()), + origins: None, ori: Quaternion::identity(), }); } @@ -224,7 +224,9 @@ impl<'a> PhysicsData<'a> { let flat_radius = collider.map(|c| c.get_radius()).unwrap_or(0.5) * scale; let neighborhood_radius = match collider { Some(Collider::CapsulePrism { radius, .. }) => radius * scale, - _ => flat_radius, + Some(Collider::Box { .. } | Collider::Voxel { .. } | Collider::Point) | None => { + flat_radius + }, }; let radius = (flat_radius.powi(2) + half_height.powi(2)).sqrt(); @@ -237,31 +239,36 @@ impl<'a> PhysicsData<'a> { phys_cache.scaled_radius = flat_radius; let ori = ori.to_quat(); - let (p0, p1) = match collider { + let origins = match collider { Some(Collider::CapsulePrism { p0, p1, .. }) => { - // Build the line between two origins + // Apply orientation to origins of prism. + // We do this by building line between them, + // rotate it and then split back to origins. + // + // (Otherwise we will need to do the same with each + // origin). let a = p1 - p0; let len = a.magnitude(); - // Make it 3d - let a = Vec3::new(a.x, a.y, 0.0); - // Rotate it + // We cast it to 3d and then convert it back to 2d + // to apply quaternion. + let a = a.with_z(0.0); let a = ori * a; - // Make it 2d again - let a = Vec2::new(a.x, a.y); + let a = a.xy(); + // Previous operation could shrink x and y coordinates + // if orientation had Z parameter. // Make sure we have the same length as before - // (and scale it) + // (and scale it, while we on it). let a = a * scale * len / a.magnitude(); - - // Splite the oriented line into two origins let p0 = -a / 2.0; let p1 = a / 2.0; - (p0, p1) + + Some((p0, p1)) }, Some(Collider::Box { .. } | Collider::Voxel { .. } | Collider::Point) | None => { - (Vec2::zero(), Vec2::zero()) + None }, }; - phys_cache.origins = (p0, p1); + phys_cache.origins = origins; phys_cache.ori = ori; } } @@ -350,7 +357,7 @@ impl<'a> PhysicsData<'a> { mass, collider, sticky, - physics, + mut physics, projectile, char_state_maybe, )| { @@ -443,113 +450,37 @@ impl<'a> PhysicsData<'a> { for i in 0..increments { let factor = i as f32 * step_delta; - - // get positions - let pos = pos.0 + previous_cache.velocity_dt * factor; - - let pos_other = - pos_other.0 + previous_cache_other.velocity_dt * factor; - - // Compare Z ranges - let ceiling = pos.z + z_limits.1 * previous_cache.scale; - let floor = pos.z + z_limits.0 * previous_cache.scale; - - let ceiling_other = - pos_other.z + z_limits_other.1 * previous_cache_other.scale; - let floor_other = - pos_other.z + z_limits_other.0 * previous_cache_other.scale; - let in_z_range = - ceiling >= floor_other && floor <= ceiling_other; - - // check horizontal distance - let (p0_offset, p1_offset) = previous_cache.origins; - let p0 = pos + p0_offset; - let p1 = pos + p1_offset; - let segment = LineSegment2 { - start: p0.xy(), - end: p1.xy(), - }; - - let (p0_offset_other, p1_offset_other) = - previous_cache_other.origins; - let p0_other = pos_other + p0_offset_other; - let p1_other = pos_other + p1_offset_other; - - let segment_other = LineSegment2 { - start: p0_other.xy(), - end: p1_other.xy(), - }; - - let diff = projection_between(segment, segment_other); - - let in_collision_range = - diff.magnitude_squared() <= collision_dist.powi(2); - - let collides = in_collision_range && in_z_range; - - if !collides { - continue; - } - - // If entities have not yet collided - // this tick (but just did) and if 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) { - physics.touch_entities.insert(*other); - entity_entity_collisions += 1; - } - - // Don't apply e2e pushback to entities - // that are in a forced movement state - // (e.g. roll, leapmelee). - // - // This allows leaps to work properly - // (since you won't get pushed away before - // delivering the hit), and allows - // rolling through an enemy when trapped - // (e.g. with minotaur). - // - // This allows using e2e pushback to - // gain speed by jumping out of a roll - // while in the middle of a collider, this - // is an intentional combat mechanic. - let forced_movement = matches!( + match try_e2e_collision( + // utility variables for our entity + &mut collision_registered, + &mut entity_entity_collisions, + factor, + collision_dist, + &mut physics, char_state_maybe, - Some(cs) if cs.is_forced_movement()); - - // Don't apply repulsive force - // to projectiles - // - // or if we're colliding with a - // terrain-like entity, - // - // or if we are a terrain-like entity - // - // Don't apply force when entity - // is a sticky which is on the - // ground (or on the wall) - if !forced_movement - && (!is_sticky || is_mid_air) - && diff.magnitude_squared() > 0.0 - && !is_projectile - && !matches!(collider_other, Some(Collider::Voxel { .. })) - && !matches!(collider, Some(Collider::Voxel { .. })) - { - let force = 400.0 - * (collision_dist - diff.magnitude()) - * mass_other.0 - / (mass.0 + mass_other.0); - - vel_delta += - Vec3::from(diff.normalized()) * force * step_delta; + &mut vel_delta, + step_delta, + // physics flags + is_mid_air, + is_sticky, + is_projectile, + // entity we colliding with + *other, + // symetrical collider context + pos, + pos_other, + previous_cache, + previous_cache_other, + z_limits, + z_limits_other, + collider, + collider_other, + *mass, + *mass_other, + ) { + ControlFlow::Continue(..) => continue, + ControlFlow::Break(..) => break, } - - collision_registered = true; } }, ); @@ -1861,31 +1792,161 @@ fn voxel_collider_bounding_sphere( } } -/// Get the shortest line segment between AB and CD, used for pushback -/// and collision checks. -fn projection_between(ab: LineSegment2, cd: LineSegment2) -> Vec2 { - // On 2d we can just check projection of A to CD, B to CD, C to AB and D to AB. +#[allow(clippy::too_many_arguments)] +fn try_e2e_collision( + // utility variables for our entity + collision_registered: &mut bool, + entity_entity_collisions: &mut u64, + factor: f32, + collision_dist: f32, + physics: &mut PhysicsState, + char_state_maybe: Option<&CharacterState>, + vel_delta: &mut Vec3, + step_delta: f32, + // physics flags + is_mid_air: bool, + is_sticky: bool, + is_projectile: bool, + // entity we colliding with + other: Uid, + // symetrical collider context + pos: &Pos, + pos_other: &Pos, + previous_cache: &PreviousPhysCache, + previous_cache_other: &PreviousPhysCache, + z_limits: (f32, f32), + z_limits_other: (f32, f32), + collider: Option<&Collider>, + collider_other: Option<&Collider>, + mass: Mass, + mass_other: Mass, +) -> ControlFlow<()> { + // Find the distance betwen our collider and + // collider we collide with and get vector of pushback. // - // NOTE: We don't check if segments are intersecting, because - // even if they do, we still need to return pushback vector. - let a = ab.start; - let b = ab.end; - let c = cd.start; - let d = cd.end; + // If we aren't colliding, just skip step. - let projections = [ - // A to CD - a - cd.projected_point(a), - // B to CD - b - cd.projected_point(b), - // C to AB - c - ab.projected_point(c), - // D to AB - d - ab.projected_point(d), - ]; + // Get positions + let pos = pos.0 + previous_cache.velocity_dt * factor; + let pos_other = pos_other.0 + previous_cache_other.velocity_dt * factor; - // min_by_key returns None only if iterator is empty, so unwrap is fine - IntoIterator::into_iter(projections) - .min_by_key(|p| ordered_float::OrderedFloat(p.magnitude_squared())) - .unwrap() + // Compare Z ranges + let ceiling = pos.z + z_limits.1 * previous_cache.scale; + let floor = pos.z + z_limits.0 * previous_cache.scale; + + let ceiling_other = pos_other.z + z_limits_other.1 * previous_cache_other.scale; + let floor_other = pos_other.z + z_limits_other.0 * previous_cache_other.scale; + + let in_z_range = ceiling >= floor_other && floor <= ceiling_other; + + // Check horizontal distance. + let diff = if in_z_range { + let ours = ColliderContext { + pos, + previous_cache, + }; + let theirs = ColliderContext { + pos: pos_other, + previous_cache: previous_cache_other, + }; + + projection_between(ours, theirs) + } else { + return ControlFlow::Continue(()); + }; + + let in_collision_range = diff.magnitude_squared() <= collision_dist.powi(2); + if !in_collision_range { + return ControlFlow::Continue(()); + } + + // If entities have not yet collided this tick (but just did) and if 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) { + physics.touch_entities.insert(other); + *entity_entity_collisions += 1; + } + + // Don't apply e2e pushback to entities that are in a forced movement state + // (e.g. roll, leapmelee). + // + // This allows leaps to work properly (since you won't get pushed away + // before delivering the hit), and allows rolling through an enemy when + // trapped (e.g. with minotaur). + // + // This allows using e2e pushback to gain speed by jumping out of a roll + // while in the middle of a collider, this is an intentional combat mechanic. + let forced_movement = matches!(char_state_maybe, Some(cs) if cs.is_forced_movement()); + + // Don't apply repulsive force to projectiles, + // or if we're colliding with a terrain-like entity, + // or if we are a terrain-like entity. + // + // Don't apply force when entity is a sticky which is on the ground + // (or on the wall). + if !forced_movement + && (!is_sticky || is_mid_air) + && diff.magnitude_squared() > 0.0 + && !is_projectile + && !matches!(collider_other, Some(Collider::Voxel { .. })) + && !matches!(collider, Some(Collider::Voxel { .. })) + { + let force = + 400.0 * (collision_dist - diff.magnitude()) * mass_other.0 / (mass.0 + mass_other.0); + + *vel_delta += Vec3::from(diff.normalized()) * force * step_delta; + } + + *collision_registered = true; + + ControlFlow::Continue(()) +} + +struct ColliderContext<'a> { + pos: Vec3, + previous_cache: &'a PreviousPhysCache, +} + +/// Find pushback vector +fn projection_between(c0: ColliderContext, c1: ColliderContext) -> Vec2 { + // "Proper" way to do this would be handle the case when both our colliders + // are capsule prisms by building origins from p0, p1 offsets and our + // positions and find some sort of projection between line segments of + // both colliders. + // While it's possible, it's not a trivial operation especially + // in the case when they are intersect. Because in such case, + // even when you found intersection and you should push entities back + // from each other, you get then difference between them is 0 vector. + // + // Considering that we won't fully simulate collision of capsule prism. + // As intermediate solution, we would assume that bigger collider + // (with bigger scaled_radius) is capsule prism (cylinder is special + // case of capsule prism too) and smaller collider is cylinder (point is + // special case of cylinder). + if c0.previous_cache.scaled_radius > c1.previous_cache.scaled_radius { + let (p0_offset, p1_offset) = c0 + .previous_cache + .origins + .unwrap_or((Vec2::zero(), Vec2::zero())); + let segment = LineSegment2 { + start: c0.pos.xy() + p0_offset, + end: c0.pos.xy() + p1_offset, + }; + let other = c1.pos.xy(); + + other - segment.projected_point(other) + } else { + let we = c0.pos.xy(); + let (p0_offset_other, p1_offset_other) = c1 + .previous_cache + .origins + .unwrap_or((Vec2::zero(), Vec2::zero())); + let segment_other = LineSegment2 { + start: c1.pos.xy() + p0_offset_other, + end: c1.pos.xy() + p1_offset_other, + }; + + we - segment_other.projected_point(we) + } } diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index c7e40f42dc..03364636bd 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -216,7 +216,7 @@ impl StateExt for State { z_min: 0.0, z_max: body.height(), } - } + }, }) .with(comp::Controller::default()) .with(body) From 7d97fe7ec50da0f3a9bdbc42335ad9c664bb6ae5 Mon Sep 17 00:00:00 2001 From: juliancoffee Date: Thu, 16 Sep 2021 03:08:54 +0300 Subject: [PATCH 07/13] Fix colliding bugs - Make cylinder-like capsules prisms work without NaN in origin offsets. Just return p0 as both origins instead of getting NaN by further normalizing required because of how we rotate offsets. - Fix pushback direction. Make sure that pushback is calculated as our_pos - their_pos (and not other way around). - Fix colliding boundary detection. Calculate center as Vec3::new(0, 0, height) and not as `Vec3::new(0, height, 0)`. --- common/systems/src/phys.rs | 187 ++++++++++++++++++++++--------------- 1 file changed, 111 insertions(+), 76 deletions(-) diff --git a/common/systems/src/phys.rs b/common/systems/src/phys.rs index 76325ddb15..c11f57abcd 100644 --- a/common/systems/src/phys.rs +++ b/common/systems/src/phys.rs @@ -216,53 +216,66 @@ impl<'a> PhysicsData<'a> { { let scale = scale.map(|s| s.0).unwrap_or(1.0); let z_limits = calc_z_limit(cs, collider); - let z_limits = (z_limits.0 * scale, z_limits.1 * scale); - let half_height = (z_limits.1 - z_limits.0) / 2.0; + let (z_min, z_max) = z_limits; + let (z_min, z_max) = (z_min * scale, z_max * scale); + let half_height = (z_max - z_min) / 2.0; phys_cache.velocity_dt = vel.0 * self.read.dt.0; - let entity_center = position.0 + Vec3::new(0.0, z_limits.0 + half_height, 0.0); - let flat_radius = collider.map(|c| c.get_radius()).unwrap_or(0.5) * scale; + let entity_center = position.0 + Vec3::new(0.0, 0.0, z_min + half_height); + let flat_radius = collider.map_or(0.5, Collider::get_radius) * scale; + let radius = (flat_radius.powi(2) + half_height.powi(2)).sqrt(); + + // Move center to the middle between OLD and OLD+VEL_DT + // so that we can reduce the collision_boundary. + phys_cache.center = entity_center + phys_cache.velocity_dt / 2.0; + phys_cache.collision_boundary = radius + + (phys_cache.velocity_dt / 2.0).magnitude(); + phys_cache.scale = scale; + phys_cache.scaled_radius = flat_radius; + let neighborhood_radius = match collider { Some(Collider::CapsulePrism { radius, .. }) => radius * scale, Some(Collider::Box { .. } | Collider::Voxel { .. } | Collider::Point) | None => { flat_radius }, }; - let radius = (flat_radius.powi(2) + half_height.powi(2)).sqrt(); - - // Move center to the middle between OLD and OLD+VEL_DT so that we can reduce - // the collision_boundary - phys_cache.center = entity_center + phys_cache.velocity_dt / 2.0; - phys_cache.collision_boundary = radius + (phys_cache.velocity_dt / 2.0).magnitude(); - phys_cache.scale = scale; phys_cache.neighborhood_radius = neighborhood_radius; - phys_cache.scaled_radius = flat_radius; let ori = ori.to_quat(); let origins = match collider { Some(Collider::CapsulePrism { p0, p1, .. }) => { - // Apply orientation to origins of prism. - // We do this by building line between them, - // rotate it and then split back to origins. - // - // (Otherwise we will need to do the same with each - // origin). let a = p1 - p0; let len = a.magnitude(); - // We cast it to 3d and then convert it back to 2d - // to apply quaternion. - let a = a.with_z(0.0); - let a = ori * a; - let a = a.xy(); - // Previous operation could shrink x and y coordinates - // if orientation had Z parameter. - // Make sure we have the same length as before - // (and scale it, while we on it). - let a = a * scale * len / a.magnitude(); - let p0 = -a / 2.0; - let p1 = a / 2.0; + // If origins are close enough, our capsule prism is cylinder + // with one origin which we don't even need to rotate. + // + // Other advantage of early-return is that we don't + // later divide by zero and return NaN + if len < 0.000001 { + Some((*p0, *p0)) + } else { + // Apply orientation to origins of prism. + // + // We do this by building line between them, + // rotate it and then split back to origins. + // (Otherwise we will need to do the same with each + // origin). + // + // Cast it to 3d and then convert it back to 2d + // to apply quaternion. + let a = a.with_z(0.0); + let a = ori * a; + let a = a.xy(); + // Previous operation could shrink x and y coordinates + // if orientation had Z parameter. + // Make sure we have the same length as before + // (and scale it, while we on it). + let a = a.normalized() * scale * len; + let p0 = -a / 2.0; + let p1 = a / 2.0; - Some((p0, p1)) + Some((p0, p1)) + } }, Some(Collider::Box { .. } | Collider::Voxel { .. } | Collider::Point) | None => { None @@ -434,6 +447,7 @@ impl<'a> PhysicsData<'a> { entity_entity_collision_checks += 1; const MIN_COLLISION_DIST: f32 = 0.3; + let increments = ((previous_cache.velocity_dt - previous_cache_other.velocity_dt) .magnitude() @@ -445,8 +459,6 @@ impl<'a> PhysicsData<'a> { let mut collision_registered = false; - let collision_dist = previous_cache.neighborhood_radius - + previous_cache_other.neighborhood_radius; for i in 0..increments { let factor = i as f32 * step_delta; @@ -455,7 +467,6 @@ impl<'a> PhysicsData<'a> { &mut collision_registered, &mut entity_entity_collisions, factor, - collision_dist, &mut physics, char_state_maybe, &mut vel_delta, @@ -1798,7 +1809,6 @@ fn try_e2e_collision( collision_registered: &mut bool, entity_entity_collisions: &mut u64, factor: f32, - collision_dist: f32, physics: &mut PhysicsState, char_state_maybe: Option<&CharacterState>, vel_delta: &mut Vec3, @@ -1831,31 +1841,31 @@ fn try_e2e_collision( let pos_other = pos_other.0 + previous_cache_other.velocity_dt * factor; // Compare Z ranges - let ceiling = pos.z + z_limits.1 * previous_cache.scale; - let floor = pos.z + z_limits.0 * previous_cache.scale; + let (z_min, z_max) = z_limits; + let ceiling = pos.z + z_max * previous_cache.scale; + let floor = pos.z + z_min * previous_cache.scale; - let ceiling_other = pos_other.z + z_limits_other.1 * previous_cache_other.scale; - let floor_other = pos_other.z + z_limits_other.0 * previous_cache_other.scale; + let (z_min_other, z_max_other) = z_limits_other; + let ceiling_other = pos_other.z + z_max_other * previous_cache_other.scale; + let floor_other = pos_other.z + z_min_other * previous_cache_other.scale; let in_z_range = ceiling >= floor_other && floor <= ceiling_other; - // Check horizontal distance. - let diff = if in_z_range { - let ours = ColliderContext { - pos, - previous_cache, - }; - let theirs = ColliderContext { - pos: pos_other, - previous_cache: previous_cache_other, - }; - - projection_between(ours, theirs) - } else { + if !in_z_range { return ControlFlow::Continue(()); - }; + } + let ours = ColliderContext { + pos, + previous_cache, + }; + let theirs = ColliderContext { + pos: pos_other, + previous_cache: previous_cache_other, + }; + let (diff, collision_dist) = projection_between(ours, theirs); let in_collision_range = diff.magnitude_squared() <= collision_dist.powi(2); + if !in_collision_range { return ControlFlow::Continue(()); } @@ -1892,8 +1902,9 @@ fn try_e2e_collision( && !matches!(collider_other, Some(Collider::Voxel { .. })) && !matches!(collider, Some(Collider::Voxel { .. })) { - let force = - 400.0 * (collision_dist - diff.magnitude()) * mass_other.0 / (mass.0 + mass_other.0); + let mass_coefficient = mass_other.0 / (mass.0 + mass_other.0); + let distance_coefficient = collision_dist - diff.magnitude(); + let force = 400.0 * distance_coefficient * mass_coefficient; *vel_delta += Vec3::from(diff.normalized()) * force * step_delta; } @@ -1908,8 +1919,8 @@ struct ColliderContext<'a> { previous_cache: &'a PreviousPhysCache, } -/// Find pushback vector -fn projection_between(c0: ColliderContext, c1: ColliderContext) -> Vec2 { +/// Find pushback vector and collision_distance we assume between this colliders. +fn projection_between(c0: ColliderContext, c1: ColliderContext) -> (Vec2, f32) { // "Proper" way to do this would be handle the case when both our colliders // are capsule prisms by building origins from p0, p1 offsets and our // positions and find some sort of projection between line segments of @@ -1924,29 +1935,53 @@ fn projection_between(c0: ColliderContext, c1: ColliderContext) -> Vec2 { // (with bigger scaled_radius) is capsule prism (cylinder is special // case of capsule prism too) and smaller collider is cylinder (point is // special case of cylinder). + // So in the end our model of collision and pushback vector is simplified + // to checking distance of the point between segment of capsule. + // + // NOTE: no matter if we consider our collider capsule prism or cylinder + // we should always build pushback vector to have direction + // of motion from our target collider to our collider. + // + // TODO: can code beloew be deduplicated? :think: if c0.previous_cache.scaled_radius > c1.previous_cache.scaled_radius { - let (p0_offset, p1_offset) = c0 - .previous_cache - .origins - .unwrap_or((Vec2::zero(), Vec2::zero())); - let segment = LineSegment2 { - start: c0.pos.xy() + p0_offset, - end: c0.pos.xy() + p1_offset, - }; + let our_radius = c0.previous_cache.neighborhood_radius; + let their_radius = c1.previous_cache.scaled_radius; + let collision_dist = our_radius + their_radius; + + let we = c0.pos.xy(); let other = c1.pos.xy(); - other - segment.projected_point(other) - } else { - let we = c0.pos.xy(); - let (p0_offset_other, p1_offset_other) = c1 - .previous_cache - .origins - .unwrap_or((Vec2::zero(), Vec2::zero())); - let segment_other = LineSegment2 { - start: c1.pos.xy() + p0_offset_other, - end: c1.pos.xy() + p1_offset_other, + let (p0_offset, p1_offset) = match c0.previous_cache.origins { + Some(origins) => origins, + None => return (we - other, collision_dist), + }; + let segment = LineSegment2 { + start: we + p0_offset, + end: we + p1_offset, }; - we - segment_other.projected_point(we) + let projection = other - segment.projected_point(other) - other; + + (projection, collision_dist) + } else { + let our_radius = c0.previous_cache.scaled_radius; + let their_radius = c1.previous_cache.neighborhood_radius; + let collision_dist = our_radius + their_radius; + + let we = c0.pos.xy(); + let other = c1.pos.xy(); + + let (p0_offset_other, p1_offset_other) = match c1.previous_cache.origins { + Some(origins) => origins, + None => return (we - other, collision_dist), + }; + let segment_other = LineSegment2 { + start: other + p0_offset_other, + end: other + p1_offset_other, + }; + + let projection = we - segment_other.projected_point(we); + + (projection, collision_dist) } } From c069a3523d95f823a8a6b5ef5216562b53761fe9 Mon Sep 17 00:00:00 2001 From: juliancoffee Date: Thu, 16 Sep 2021 13:42:07 +0300 Subject: [PATCH 08/13] Fix beam offsets - Introduce notion of min and max radius for Body instead of old `radius()` function (which is renamed to `max_radius()`). --- common/src/comp/body.rs | 14 +- common/src/states/basic_beam.rs | 3 +- common/src/states/charged_melee.rs | 2 +- common/src/states/utils.rs | 2 +- common/systems/src/beam.rs | 335 ++++++++++++----------- common/systems/src/melee.rs | 5 +- common/systems/src/phys.rs | 7 +- common/systems/src/shockwave.rs | 3 +- server/src/events/entity_manipulation.rs | 4 +- server/src/state_ext.rs | 8 +- server/src/sys/agent.rs | 10 +- voxygen/src/scene/particle.rs | 16 +- voxygen/src/session/mod.rs | 5 +- 13 files changed, 224 insertions(+), 190 deletions(-) diff --git a/common/src/comp/body.rs b/common/src/comp/body.rs index acc747b49c..291eeee15e 100644 --- a/common/src/comp/body.rs +++ b/common/src/comp/body.rs @@ -400,9 +400,17 @@ impl Body { // 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 // accurate collision shapes. - pub fn radius(&self) -> f32 { + pub fn max_radius(&self) -> f32 { let dim = self.dimensions(); - dim.x.max(dim.y) / 2.0 + let (x, y) = (dim.x, dim.y); + + x.max(y) / 2.0 + } + + pub fn min_radius(&self) -> f32 { + let (_p0, _p1, radius) = self.sausage(); + + radius } /// Base of our Capsule Prism used for collisions. @@ -453,7 +461,7 @@ impl Body { // lead to that both entities will try to keep 5.0 units away from each // other. pub fn spacing_radius(&self) -> f32 { - self.radius() + self.max_radius() + match self { Body::QuadrupedSmall(body) => match body.species { quadruped_small::Species::Rat => 0.0, diff --git a/common/src/states/basic_beam.rs b/common/src/states/basic_beam.rs index 751cc105ed..9b700b3b1d 100644 --- a/common/src/states/basic_beam.rs +++ b/common/src/states/basic_beam.rs @@ -232,8 +232,9 @@ fn height_offset(body: &Body, look_dir: Dir) -> f32 { } pub fn beam_offsets(body: &Body, look_dir: Dir, ori: Vec3) -> Vec3 { - let body_radius = body.radius(); + let body_radius = body.min_radius(); let body_offsets_z = height_offset(body, look_dir); + Vec3::new( body_radius * ori.x * 1.1, body_radius * ori.y * 1.1, diff --git a/common/src/states/charged_melee.rs b/common/src/states/charged_melee.rs index f53e726466..9ed6d641ff 100644 --- a/common/src/states/charged_melee.rs +++ b/common/src/states/charged_melee.rs @@ -196,7 +196,7 @@ impl CharacterBehavior for Data { Outcome::GroundSlam { pos: data.pos.0 + *data.ori.look_dir() - * (data.body.radius() + self.static_data.range), + * (data.body.max_radius() + self.static_data.range), }, )); } diff --git a/common/src/states/utils.rs b/common/src/states/utils.rs index c197607744..24bb320f3d 100644 --- a/common/src/states/utils.rs +++ b/common/src/states/utils.rs @@ -641,7 +641,7 @@ pub fn handle_manipulate_loadout( // MAX_PICKUP_RANGE and the radius of the body let sprite_range_check = |pos: Vec3| { (sprite_pos_f32 - pos).magnitude_squared() - < (MAX_PICKUP_RANGE + data.body.radius()).powi(2) + < (MAX_PICKUP_RANGE + data.body.max_radius()).powi(2) }; // Checks if player's feet or head is near to sprite diff --git a/common/systems/src/beam.rs b/common/systems/src/beam.rs index cb379e95d4..5a1e0d75cb 100644 --- a/common/systems/src/beam.rs +++ b/common/systems/src/beam.rs @@ -82,174 +82,193 @@ impl<'a> System<'a> for Sys { &beam_segments, ) .par_join() - .fold(|| (Vec::new(), Vec::new(), Vec::new()), - |(mut server_events, mut add_hit_entities, mut outcomes), - (entity, pos, ori, beam_segment)| - { - let creation_time = match beam_segment.creation { - Some(time) => time, - // Skip newly created beam segments - None => return (server_events, add_hit_entities, outcomes), - }; - let end_time = creation_time + beam_segment.duration.as_secs_f64(); - - let beam_owner = beam_segment - .owner - .and_then(|uid| read_data.uid_allocator.retrieve_entity_internal(uid.into())); - - let mut rng = thread_rng(); - if rng.gen_bool(0.005) { - server_events.push(ServerEvent::Sound { - sound: Sound::new(SoundKind::Beam, pos.0, 7.0, time), - }); - } - - // If beam segment is out of time emit destroy event but still continue since it - // may have traveled and produced effects a bit before reaching its - // end point - if end_time < time { - server_events.push(ServerEvent::Delete(entity)); - } - - // Determine area that was covered by the beam in the last tick - let frame_time = dt.min((end_time - time) as f32); - if frame_time <= 0.0 { - return (server_events, add_hit_entities, outcomes); - } - // Note: min() probably uneeded - let time_since_creation = (time - creation_time) as f32; - let frame_start_dist = - (beam_segment.speed * (time_since_creation - frame_time)).max(0.0); - let frame_end_dist = (beam_segment.speed * time_since_creation).max(frame_start_dist); - - // Group to ignore collisions with - // Might make this more nuanced if beams are used for non damage effects - let group = beam_owner.and_then(|e| read_data.groups.get(e)); - - let hit_entities = if let Some(beam) = beam_owner.and_then(|e| beams.get(e)) { - &beam.hit_entities - } else { - return (server_events, add_hit_entities, outcomes); - }; - - // Go through all affectable entities by querying the spatial grid - let target_iter = read_data - .cached_spatial_grid - .0 - .in_circle_aabr(pos.0.xy(), frame_end_dist - frame_start_dist) - .filter_map(|target|{ - read_data - .positions - .get(target) - .and_then(|l| read_data.healths.get(target).map(|r| (l,r))) - .and_then(|l| read_data.uids.get(target).map(|r| (l,r))) - .and_then(|l| read_data.bodies.get(target).map(|r| (l,r))) - .map(|(((pos_b, health_b), uid_b), body_b)| { - (target, uid_b, pos_b, health_b, body_b) - }) - }); - target_iter.for_each(|(target, uid_b, pos_b, health_b, body_b)| { - // Check to see if entity has already been hit recently - if hit_entities.iter().any(|&uid| uid == *uid_b) { - return; - } - - // Scales - let scale_b = read_data.scales.get(target).map_or(1.0, |s| s.0); - let rad_b = body_b.radius() * scale_b; - let height_b = body_b.height() * scale_b; - - // Check if it is a hit - let hit = entity != target && !health_b.is_dead - // Collision shapes - && sphere_wedge_cylinder_collision(pos.0, frame_start_dist, frame_end_dist, *ori.look_dir(), beam_segment.angle, pos_b.0, rad_b, height_b); - - // Finally, ensure that a hit has actually occurred by performing a raycast. We do this last because - // it's likely to be the most expensive operation. - let tgt_dist = pos.0.distance(pos_b.0); - let hit = hit && read_data.terrain - .ray(pos.0, pos.0 + *ori.look_dir() * (tgt_dist + 1.0)) - .until(|b| b.is_filled()) - .cast().0 >= tgt_dist; - - if hit { - // See if entities are in the same group - let same_group = group - .map(|group_a| Some(group_a) == read_data.groups.get(target)) - .unwrap_or(Some(*uid_b) == beam_segment.owner); - - let target_group = if same_group { - GroupTarget::InGroup - } else { - GroupTarget::OutOfGroup + .fold( + || (Vec::new(), Vec::new(), Vec::new()), + |(mut server_events, mut add_hit_entities, mut outcomes), + (entity, pos, ori, beam_segment)| { + let creation_time = match beam_segment.creation { + Some(time) => time, + // Skip newly created beam segments + None => return (server_events, add_hit_entities, outcomes), }; + let end_time = creation_time + beam_segment.duration.as_secs_f64(); - // If owner, shouldn't heal or damage - if Some(*uid_b) == beam_segment.owner { - return; + let beam_owner = beam_segment.owner.and_then(|uid| { + read_data.uid_allocator.retrieve_entity_internal(uid.into()) + }); + + let mut rng = thread_rng(); + if rng.gen_bool(0.005) { + server_events.push(ServerEvent::Sound { + sound: Sound::new(SoundKind::Beam, pos.0, 7.0, time), + }); } - let attacker_info = - beam_owner - .zip(beam_segment.owner) - .map(|(entity, uid)| AttackerInfo { - entity, - uid, - energy: read_data.energies.get(entity), - combo: read_data.combos.get(entity), - inventory: read_data.inventories.get(entity), - }); + // If beam segment is out of time emit destroy event but still continue since it + // may have traveled and produced effects a bit before reaching its + // end point + if end_time < time { + server_events.push(ServerEvent::Delete(entity)); + } - let target_info = TargetInfo { - entity: target, - uid: *uid_b, - inventory: read_data.inventories.get(target), - stats: read_data.stats.get(target), - health: read_data.healths.get(target), - pos: pos_b.0, - ori: read_data.orientations.get(target), - char_state: read_data.character_states.get(target), + // Determine area that was covered by the beam in the last tick + let frame_time = dt.min((end_time - time) as f32); + if frame_time <= 0.0 { + return (server_events, add_hit_entities, outcomes); + } + // Note: min() probably uneeded + let time_since_creation = (time - creation_time) as f32; + let frame_start_dist = + (beam_segment.speed * (time_since_creation - frame_time)).max(0.0); + let frame_end_dist = + (beam_segment.speed * time_since_creation).max(frame_start_dist); + + // Group to ignore collisions with + // Might make this more nuanced if beams are used for non damage effects + let group = beam_owner.and_then(|e| read_data.groups.get(e)); + + let hit_entities = if let Some(beam) = beam_owner.and_then(|e| beams.get(e)) { + &beam.hit_entities + } else { + return (server_events, add_hit_entities, outcomes); }; + // Go through all affectable entities by querying the spatial grid + let target_iter = read_data + .cached_spatial_grid + .0 + .in_circle_aabr(pos.0.xy(), frame_end_dist - frame_start_dist) + .filter_map(|target| { + read_data + .positions + .get(target) + .and_then(|l| read_data.healths.get(target).map(|r| (l, r))) + .and_then(|l| read_data.uids.get(target).map(|r| (l, r))) + .and_then(|l| read_data.bodies.get(target).map(|r| (l, r))) + .map(|(((pos_b, health_b), uid_b), body_b)| { + (target, uid_b, pos_b, health_b, body_b) + }) + }); + target_iter.for_each(|(target, uid_b, pos_b, health_b, body_b)| { + // Check to see if entity has already been hit recently + if hit_entities.iter().any(|&uid| uid == *uid_b) { + return; + } - // PvP check - let may_harm = combat::may_harm( - &read_data.alignments, - &read_data.players, - &read_data.uid_allocator, - beam_owner, - target, - ); - let attack_options = AttackOptions { - // No luck with dodging beams - target_dodging: false, - may_harm, - target_group, - }; + // Scales + let scale_b = read_data.scales.get(target).map_or(1.0, |s| s.0); + let rad_b = body_b.max_radius() * scale_b; + let height_b = body_b.height() * scale_b; - beam_segment.properties.attack.apply_attack( - attacker_info, - target_info, - ori.look_dir(), - attack_options, - 1.0, - AttackSource::Beam, - |e| server_events.push(e), - |o| outcomes.push(o), - ); + // Check if it is a hit + // TODO: use Capsule Prism instead of cylinder + let hit = entity != target + && !health_b.is_dead + && sphere_wedge_cylinder_collision( + pos.0, + frame_start_dist, + frame_end_dist, + *ori.look_dir(), + beam_segment.angle, + pos_b.0, + rad_b, + height_b, + ); - add_hit_entities.push((beam_owner, *uid_b)); - } - }); - (server_events, add_hit_entities, outcomes) - }).reduce(|| (Vec::new(), Vec::new(), Vec::new()), - |(mut events_a, mut hit_entities_a, mut outcomes_a), - (mut events_b, mut hit_entities_b, mut outcomes_b)| { - events_a.append(&mut events_b); - hit_entities_a.append(&mut hit_entities_b); - outcomes_a.append(&mut outcomes_b); - (events_a, hit_entities_a, outcomes_a) - }); + // Finally, ensure that a hit has actually occurred by performing a raycast. + // We do this last because it's likely to be the + // most expensive operation. + let tgt_dist = pos.0.distance(pos_b.0); + let hit = hit + && read_data + .terrain + .ray(pos.0, pos.0 + *ori.look_dir() * (tgt_dist + 1.0)) + .until(|b| b.is_filled()) + .cast() + .0 + >= tgt_dist; + + if hit { + // See if entities are in the same group + let same_group = group + .map(|group_a| Some(group_a) == read_data.groups.get(target)) + .unwrap_or(Some(*uid_b) == beam_segment.owner); + + let target_group = if same_group { + GroupTarget::InGroup + } else { + GroupTarget::OutOfGroup + }; + + // If owner, shouldn't heal or damage + if Some(*uid_b) == beam_segment.owner { + return; + } + + let attacker_info = + beam_owner.zip(beam_segment.owner).map(|(entity, uid)| { + AttackerInfo { + entity, + uid, + energy: read_data.energies.get(entity), + combo: read_data.combos.get(entity), + inventory: read_data.inventories.get(entity), + } + }); + + let target_info = TargetInfo { + entity: target, + uid: *uid_b, + inventory: read_data.inventories.get(target), + stats: read_data.stats.get(target), + health: read_data.healths.get(target), + pos: pos_b.0, + ori: read_data.orientations.get(target), + char_state: read_data.character_states.get(target), + }; + + // PvP check + let may_harm = combat::may_harm( + &read_data.alignments, + &read_data.players, + &read_data.uid_allocator, + beam_owner, + target, + ); + let attack_options = AttackOptions { + // No luck with dodging beams + target_dodging: false, + may_harm, + target_group, + }; + + beam_segment.properties.attack.apply_attack( + attacker_info, + target_info, + ori.look_dir(), + attack_options, + 1.0, + AttackSource::Beam, + |e| server_events.push(e), + |o| outcomes.push(o), + ); + + add_hit_entities.push((beam_owner, *uid_b)); + } + }); + (server_events, add_hit_entities, outcomes) + }, + ) + .reduce( + || (Vec::new(), Vec::new(), Vec::new()), + |(mut events_a, mut hit_entities_a, mut outcomes_a), + (mut events_b, mut hit_entities_b, mut outcomes_b)| { + events_a.append(&mut events_b); + hit_entities_a.append(&mut hit_entities_b); + outcomes_a.append(&mut outcomes_b); + (events_a, hit_entities_a, outcomes_a) + }, + ); job.cpu_stats.measure(ParMode::Single); outcomes.append(&mut new_outcomes); diff --git a/common/systems/src/melee.rs b/common/systems/src/melee.rs index 5067b79b53..663cf6d5c5 100644 --- a/common/systems/src/melee.rs +++ b/common/systems/src/melee.rs @@ -80,7 +80,8 @@ impl<'a> System<'a> for Sys { // Scales let eye_pos = pos.0 + Vec3::unit_z() * body.eye_height(); let scale = read_data.scales.get(attacker).map_or(1.0, |s| s.0); - let rad = body.radius() * scale; + // TODO: use Capsule Prisms instead of Cylinders + let rad = body.max_radius() * scale; // Mine blocks broken by the attack if let Some((block_pos, tool)) = melee_attack.break_block { @@ -115,7 +116,7 @@ impl<'a> System<'a> for Sys { // Scales let scale_b = read_data.scales.get(target).map_or(1.0, |s| s.0); - let rad_b = body_b.radius() * scale_b; + let rad_b = body_b.max_radius() * scale_b; // Check if entity is dodging let target_dodging = read_data diff --git a/common/systems/src/phys.rs b/common/systems/src/phys.rs index c11f57abcd..88c9b152ee 100644 --- a/common/systems/src/phys.rs +++ b/common/systems/src/phys.rs @@ -228,8 +228,7 @@ impl<'a> PhysicsData<'a> { // Move center to the middle between OLD and OLD+VEL_DT // so that we can reduce the collision_boundary. phys_cache.center = entity_center + phys_cache.velocity_dt / 2.0; - phys_cache.collision_boundary = radius - + (phys_cache.velocity_dt / 2.0).magnitude(); + phys_cache.collision_boundary = radius + (phys_cache.velocity_dt / 2.0).magnitude(); phys_cache.scale = scale; phys_cache.scaled_radius = flat_radius; @@ -459,7 +458,6 @@ impl<'a> PhysicsData<'a> { let mut collision_registered = false; - for i in 0..increments { let factor = i as f32 * step_delta; match try_e2e_collision( @@ -1919,7 +1917,8 @@ struct ColliderContext<'a> { previous_cache: &'a PreviousPhysCache, } -/// Find pushback vector and collision_distance we assume between this colliders. +/// Find pushback vector and collision_distance we assume between this +/// colliders. fn projection_between(c0: ColliderContext, c1: ColliderContext) -> (Vec2, f32) { // "Proper" way to do this would be handle the case when both our colliders // are capsule prisms by building origins from p0, p1 offsets and our diff --git a/common/systems/src/shockwave.rs b/common/systems/src/shockwave.rs index 64b735901b..fdf8d26613 100644 --- a/common/systems/src/shockwave.rs +++ b/common/systems/src/shockwave.rs @@ -152,7 +152,8 @@ impl<'a> System<'a> for Sys { // Scales let scale_b = read_data.scales.get(target).map_or(1.0, |s| s.0); - let rad_b = body_b.radius() * scale_b; + // TODO: use Capsule Prism instead of Cylinder + let rad_b = body_b.max_radius() * scale_b; // Angle checks let pos_b_ground = Vec3::new(pos_b.0.x, pos_b.0.y, pos.0.z); diff --git a/server/src/events/entity_manipulation.rs b/server/src/events/entity_manipulation.rs index 440a4efea2..1386657929 100644 --- a/server/src/events/entity_manipulation.rs +++ b/server/src/events/entity_manipulation.rs @@ -715,8 +715,8 @@ pub fn handle_explosion(server: &Server, pos: Vec3, explosion: Explosion, o cyl_body: Body, ) -> f32 { // 2d check - let horiz_dist = - Vec2::::from(sphere_pos - cyl_pos).distance(Vec2::default()) - cyl_body.radius(); + let horiz_dist = Vec2::::from(sphere_pos - cyl_pos).distance(Vec2::default()) + - cyl_body.max_radius(); // z check let half_body_height = cyl_body.height() / 2.0; let vert_distance = diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index 03364636bd..0802a3f3e9 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -249,7 +249,7 @@ impl StateExt for State { .with(body.mass()) .with(body.density()) .with(comp::Collider::Box { - radius: body.radius(), + radius: body.max_radius(), z_min: 0.0, z_max: body.height(), }) @@ -315,7 +315,7 @@ impl StateExt for State { projectile_base = projectile_base.with(comp::Collider::Point) } else { projectile_base = projectile_base.with(comp::Collider::Box { - radius: body.radius(), + radius: body.max_radius(), z_min: 0.0, z_max: body.height(), }) @@ -392,7 +392,7 @@ impl StateExt for State { .with(comp::Vel(Vec3::zero())) .with(comp::Ori::default()) .with(comp::Collider::Box { - radius: comp::Body::Object(object).radius(), + radius: comp::Body::Object(object).max_radius(), z_min: 0.0, z_max: comp::Body::Object(object).height() }) @@ -510,7 +510,7 @@ impl StateExt for State { // commands, so we can assume that all of these calls succeed, // justifying ignoring the result of insertion. self.write_component_ignore_entity_dead(entity, comp::Collider::Box { - radius: body.radius(), + radius: body.max_radius(), z_min: 0.0, z_max: body.height(), }); diff --git a/server/src/sys/agent.rs b/server/src/sys/agent.rs index 16fc0986f0..9aaaaa94e9 100644 --- a/server/src/sys/agent.rs +++ b/server/src/sys/agent.rs @@ -1783,9 +1783,9 @@ impl<'a> AgentData<'a> { // Wield the weapon as running towards the target controller.actions.push(ControlAction::Wield); - let min_attack_dist = (self.body.map_or(0.5, |b| b.radius()) + DEFAULT_ATTACK_RANGE) + let min_attack_dist = (self.body.map_or(0.5, |b| b.max_radius()) + DEFAULT_ATTACK_RANGE) * self.scale - + tgt_data.body.map_or(0.5, |b| b.radius()) * tgt_data.scale.map_or(1.0, |s| s.0); + + tgt_data.body.map_or(0.5, |b| b.max_radius()) * tgt_data.scale.map_or(1.0, |s| s.0); let dist_sqrd = self.pos.0.distance_squared(tgt_data.pos.0); let angle = self .ori @@ -3676,7 +3676,7 @@ impl<'a> AgentData<'a> { ) { const BIRD_ATTACK_RANGE: f32 = 4.0; const BIRD_CHARGE_DISTANCE: f32 = 15.0; - let bird_attack_distance = self.body.map_or(0.0, |b| b.radius()) + BIRD_ATTACK_RANGE; + let bird_attack_distance = self.body.map_or(0.0, |b| b.max_radius()) + BIRD_ATTACK_RANGE; // Increase action timer agent.action_state.timer += read_data.dt.0; // If higher than 2 blocks @@ -3748,7 +3748,7 @@ impl<'a> AgentData<'a> { const MINOTAUR_ATTACK_RANGE: f32 = 5.0; const MINOTAUR_CHARGE_DISTANCE: f32 = 15.0; let minotaur_attack_distance = - self.body.map_or(0.0, |b| b.radius()) + MINOTAUR_ATTACK_RANGE; + self.body.map_or(0.0, |b| b.max_radius()) + MINOTAUR_ATTACK_RANGE; let health_fraction = self.health.map_or(1.0, |h| h.fraction()); // Sets action counter at start of combat if agent.action_state.counter < MINOTAUR_FRENZY_THRESHOLD @@ -3815,7 +3815,7 @@ impl<'a> AgentData<'a> { const GOLEM_LASER_RANGE: f32 = 30.0; const GOLEM_LONG_RANGE: f32 = 50.0; const GOLEM_TARGET_SPEED: f32 = 8.0; - let golem_melee_range = self.body.map_or(0.0, |b| b.radius()) + GOLEM_MELEE_RANGE; + let golem_melee_range = self.body.map_or(0.0, |b| b.max_radius()) + GOLEM_MELEE_RANGE; // Fraction of health, used for activation of shockwave // If golem don't have health for some reason, assume it's full let health_fraction = self.health.map_or(1.0, |h| h.fraction()); diff --git a/voxygen/src/scene/particle.rs b/voxygen/src/scene/particle.rs index d66177fd9b..1aa4d8e540 100644 --- a/voxygen/src/scene/particle.rs +++ b/voxygen/src/scene/particle.rs @@ -198,7 +198,7 @@ impl ParticleMgr { 0.0, ) .normalized() - * (body.radius() + 4.0) + * (body.max_radius() + 4.0) + Vec3::unit_z() * (body.height() + 2.0) * rng.gen::(); Particle::new_directed( @@ -688,7 +688,7 @@ impl ParticleMgr { 0.0, ) .normalized() - * (body.radius() + 2.0) + * (body.max_radius() + 2.0) + Vec3::unit_z() * body.height() * rng.gen::(); let (start_pos, end_pos) = @@ -720,8 +720,8 @@ impl ParticleMgr { || { let start_pos = pos.0 + Vec3::new( - body.radius(), - body.radius(), + body.max_radius(), + body.max_radius(), body.height() / 2.0, ) .map(|d| d * rng.gen_range(-1.0..1.0)); @@ -1021,8 +1021,12 @@ impl ParticleMgr { + usize::from(self.scheduler.heartbeats(Duration::from_millis(15))), || { let start_pos = pos.0 - + Vec3::new(body.radius(), body.radius(), body.height() / 2.0) - .map(|d| d * rng.gen_range(-1.0..1.0)); + + Vec3::new( + body.max_radius(), + body.max_radius(), + body.height() / 2.0, + ) + .map(|d| d * rng.gen_range(-1.0..1.0)); let end_pos = start_pos + Vec3::unit_z() * body.height() + Vec3::::zero() diff --git a/voxygen/src/session/mod.rs b/voxygen/src/session/mod.rs index 7e74f38936..e09d923ebc 100644 --- a/voxygen/src/session/mod.rs +++ b/voxygen/src/session/mod.rs @@ -1612,12 +1612,13 @@ fn under_cursor( .filter_map(|(e, p, s, b, i)| { const RADIUS_SCALE: f32 = 3.0; // TODO: use collider radius instead of body radius? - let radius = s.map_or(1.0, |s| s.0) * b.radius() * RADIUS_SCALE; + let radius = s.map_or(1.0, |s| s.0) * b.max_radius() * RADIUS_SCALE; // Move position up from the feet let pos = Vec3::new(p.0.x, p.0.y, p.0.z + radius); // Distance squared from camera to the entity let dist_sqr = pos.distance_squared(cam_pos); - // We only care about interacting with entities that contain items, or are not inanimate (to trade with) + // We only care about interacting with entities that contain items, + // or are not inanimate (to trade with) if i.is_some() || !matches!(b, comp::Body::Object(_)) { Some((e, pos, radius, dist_sqr)) } else { From fdb4b7111a5fc0d264cdc7aeb8a3f15e36d1b459 Mon Sep 17 00:00:00 2001 From: juliancoffee Date: Thu, 16 Sep 2021 16:53:29 +0300 Subject: [PATCH 09/13] Fix bug with zero-pushback for bigger colliders --- common/systems/src/phys.rs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/common/systems/src/phys.rs b/common/systems/src/phys.rs index 88c9b152ee..a6ae8bfedf 100644 --- a/common/systems/src/phys.rs +++ b/common/systems/src/phys.rs @@ -1942,14 +1942,13 @@ fn projection_between(c0: ColliderContext, c1: ColliderContext) -> (Vec2, f // of motion from our target collider to our collider. // // TODO: can code beloew be deduplicated? :think: + let we = c0.pos.xy(); + let other = c1.pos.xy(); if c0.previous_cache.scaled_radius > c1.previous_cache.scaled_radius { let our_radius = c0.previous_cache.neighborhood_radius; let their_radius = c1.previous_cache.scaled_radius; let collision_dist = our_radius + their_radius; - let we = c0.pos.xy(); - let other = c1.pos.xy(); - let (p0_offset, p1_offset) = match c0.previous_cache.origins { Some(origins) => origins, None => return (we - other, collision_dist), @@ -1959,7 +1958,7 @@ fn projection_between(c0: ColliderContext, c1: ColliderContext) -> (Vec2, f end: we + p1_offset, }; - let projection = other - segment.projected_point(other) - other; + let projection = segment.projected_point(other) - other; (projection, collision_dist) } else { @@ -1967,9 +1966,6 @@ fn projection_between(c0: ColliderContext, c1: ColliderContext) -> (Vec2, f let their_radius = c1.previous_cache.neighborhood_radius; let collision_dist = our_radius + their_radius; - let we = c0.pos.xy(); - let other = c1.pos.xy(); - let (p0_offset_other, p1_offset_other) = match c1.previous_cache.origins { Some(origins) => origins, None => return (we - other, collision_dist), From 3dd6aa9deadb62370229c19610ee9e6359d15269 Mon Sep 17 00:00:00 2001 From: juliancoffee Date: Thu, 16 Sep 2021 18:25:05 +0300 Subject: [PATCH 10/13] Implement capsule2capsule collisions --- common/systems/src/phys.rs | 81 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/common/systems/src/phys.rs b/common/systems/src/phys.rs index a6ae8bfedf..0140d50a38 100644 --- a/common/systems/src/phys.rs +++ b/common/systems/src/phys.rs @@ -1920,6 +1920,87 @@ struct ColliderContext<'a> { /// Find pushback vector and collision_distance we assume between this /// colliders. fn projection_between(c0: ColliderContext, c1: ColliderContext) -> (Vec2, f32) { + const DIFF_THRESHOLD: f32 = std::f32::EPSILON; + let our_radius = c0.previous_cache.neighborhood_radius; + let their_radius = c1.previous_cache.neighborhood_radius; + let collision_dist = our_radius + their_radius; + + let we = c0.pos.xy(); + let other = c1.pos.xy(); + + let (p0_offset, p1_offset) = match c0.previous_cache.origins { + Some(origins) => origins, + // fallback to simpler model + None => return capsule2cylinder(c0, c1), + }; + let segment = LineSegment2 { + start: we + p0_offset, + end: we + p1_offset, + }; + + let (p0_offset_other, p1_offset_other) = match c1.previous_cache.origins { + Some(origins) => origins, + // fallback to simpler model + None => return capsule2cylinder(c0, c1), + }; + let segment_other = LineSegment2 { + start: other + p0_offset_other, + end: other + p1_offset_other, + }; + + let (our, their) = closest_points(segment, segment_other); + let diff = our - their; + + if diff.magnitude_squared() < DIFF_THRESHOLD { + capsule2cylinder(c0, c1) + } else { + (diff, collision_dist) + } +} + +fn closest_points(n: LineSegment2, m: LineSegment2) -> (Vec2, Vec2) { + let p0 = n.start; + let r1 = n.end - n.start; + let q0 = m.start; + let r2 = m.end - m.start; + + // The solution for following equations + // t = (q0.x + u * r2.x - p0.x) / r1.x + // u = (p0.y + t * r1.y - q0.y) / r2.y + let t = (r2.y / r2.x * (q0.x - p0.x) + p0.y - q0.y) / (r1.x * r2.y / r2.x - r1.y); + let u = (p0.y + t * r1.y - q0.y) / r2.y; + + // Check to see whether the lines are parallel + if !t.is_finite() || !u.is_finite() { + core::array::IntoIter::new([ + (n.projected_point(m.start), m.start), + (n.projected_point(m.end), m.end), + (n.start, m.projected_point(n.start)), + (n.end, m.projected_point(n.end)), + ]) + .min_by_key(|(a, b)| ordered_float::OrderedFloat(a.distance_squared(*b))) + .expect("Lines had non-finite elements") + } else { + let t = t.clamped(0.0, 1.0); + let u = u.clamped(0.0, 1.0); + + let close_n = p0 + r1 * t; + let close_m = q0 + r2 * u; + + let proj_n = n.projected_point(close_m); + let proj_m = m.projected_point(close_n); + + if proj_n.distance_squared(close_m) < proj_m.distance_squared(close_n) { + (proj_n, close_m) + } else { + (close_n, proj_m) + } + } +} + +/// Find pushback vector and collision_distance we assume between this +/// colliders assuming that only one of them is capsule prism. +fn capsule2cylinder(c0: ColliderContext, c1: ColliderContext) -> (Vec2, f32) { // "Proper" way to do this would be handle the case when both our colliders // are capsule prisms by building origins from p0, p1 offsets and our // positions and find some sort of projection between line segments of From 4e3fb875260f64c38ea797a1ca0558112e8aa53a Mon Sep 17 00:00:00 2001 From: juliancoffee Date: Thu, 16 Sep 2021 19:45:17 +0300 Subject: [PATCH 11/13] Adress review - Rename Collider::get_radius to bounding_radius - Check origin difference in CapsulePrism with EPSILON * 10.0 instead of magic 0.00001 - Add comments for closest_points, hardnened expression against NaNs - Add comments to try_e2e_collision function, renamed to resolve_e2e_collision, make it return whether collision was triggered. - Remove Collider::Box (it is Cylinder, which is subset of CapsulePrism with p0=p1=Vec2::zero()) --- common/src/comp/phys.rs | 9 +-- common/src/util/find_dist.rs | 2 +- common/systems/src/phys.rs | 128 ++++++++++++++--------------------- server/src/state_ext.rs | 55 ++++++--------- voxygen/src/scene/mod.rs | 24 ------- 5 files changed, 73 insertions(+), 145 deletions(-) diff --git a/common/src/comp/phys.rs b/common/src/comp/phys.rs index 1c3c73b698..99d68492d2 100644 --- a/common/src/comp/phys.rs +++ b/common/src/comp/phys.rs @@ -109,11 +109,6 @@ pub enum Collider { Voxel { id: String, }, - Box { - radius: f32, - z_min: f32, - z_max: f32, - }, /// Capsule prism with line segment from p0 to p1 CapsulePrism { p0: Vec2, @@ -126,10 +121,9 @@ pub enum Collider { } impl Collider { - pub fn get_radius(&self) -> f32 { + pub fn bounding_radius(&self) -> f32 { match self { Collider::Voxel { .. } => 1.0, - Collider::Box { radius, .. } => *radius, Collider::CapsulePrism { radius, p0, p1, .. } => { let a = p0.distance(*p1); a / 2.0 + *radius @@ -146,7 +140,6 @@ impl Collider { pub fn get_z_limits(&self, modifier: f32) -> (f32, f32) { match self { Collider::Voxel { .. } => (0.0, 1.0), - 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), } diff --git a/common/src/util/find_dist.rs b/common/src/util/find_dist.rs index 10399a2b63..d2a924277a 100644 --- a/common/src/util/find_dist.rs +++ b/common/src/util/find_dist.rs @@ -39,7 +39,7 @@ impl Cylinder { char_state: Option<&crate::comp::CharacterState>, ) -> Self { let scale = scale.map_or(1.0, |s| s.0); - let radius = collider.as_ref().map_or(0.5, |c| c.get_radius()) * scale; + let radius = collider.as_ref().map_or(0.5, |c| c.bounding_radius()) * scale; let z_limit_modifier = char_state .filter(|char_state| char_state.is_dodge()) .map_or(1.0, |_| 0.5) diff --git a/common/systems/src/phys.rs b/common/systems/src/phys.rs index 0140d50a38..4cf7ea8beb 100644 --- a/common/systems/src/phys.rs +++ b/common/systems/src/phys.rs @@ -23,7 +23,7 @@ use specs::{ Entities, Entity, Join, ParJoin, Read, ReadExpect, ReadStorage, SystemData, Write, WriteExpect, WriteStorage, }; -use std::ops::{ControlFlow, Range}; +use std::ops::Range; use vek::*; /// The density of the fluid as a function of submersion ratio in given fluid @@ -222,7 +222,7 @@ impl<'a> PhysicsData<'a> { phys_cache.velocity_dt = vel.0 * self.read.dt.0; let entity_center = position.0 + Vec3::new(0.0, 0.0, z_min + half_height); - let flat_radius = collider.map_or(0.5, Collider::get_radius) * scale; + let flat_radius = collider.map_or(0.5, Collider::bounding_radius) * scale; let radius = (flat_radius.powi(2) + half_height.powi(2)).sqrt(); // Move center to the middle between OLD and OLD+VEL_DT @@ -234,9 +234,7 @@ impl<'a> PhysicsData<'a> { let neighborhood_radius = match collider { Some(Collider::CapsulePrism { radius, .. }) => radius * scale, - Some(Collider::Box { .. } | Collider::Voxel { .. } | Collider::Point) | None => { - flat_radius - }, + Some(Collider::Voxel { .. } | Collider::Point) | None => flat_radius, }; phys_cache.neighborhood_radius = neighborhood_radius; @@ -250,7 +248,7 @@ impl<'a> PhysicsData<'a> { // // Other advantage of early-return is that we don't // later divide by zero and return NaN - if len < 0.000001 { + if len < std::f32::EPSILON * 10.0 { Some((*p0, *p0)) } else { // Apply orientation to origins of prism. @@ -276,9 +274,7 @@ impl<'a> PhysicsData<'a> { Some((p0, p1)) } }, - Some(Collider::Box { .. } | Collider::Voxel { .. } | Collider::Point) | None => { - None - }, + Some(Collider::Voxel { .. } | Collider::Point) | None => None, }; phys_cache.origins = origins; phys_cache.ori = ori; @@ -460,7 +456,10 @@ impl<'a> PhysicsData<'a> { for i in 0..increments { let factor = i as f32 * step_delta; - match try_e2e_collision( + // We are not interested if collision succeed + // or no as of now. + // Collision reaction is done inside. + let _ = resolve_e2e_collision( // utility variables for our entity &mut collision_registered, &mut entity_entity_collisions, @@ -486,10 +485,7 @@ impl<'a> PhysicsData<'a> { collider_other, *mass, *mass_other, - ) { - ControlFlow::Continue(..) => continue, - ControlFlow::Break(..) => break, - } + ); } }, ); @@ -824,7 +820,7 @@ impl<'a> PhysicsData<'a> { // // 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.bounding_radius() * scale * 0.1; let (z_min, z_max) = collider.get_z_limits(scale); let mut cpos = *pos; @@ -847,45 +843,6 @@ impl<'a> PhysicsData<'a> { ); tgt_pos = cpos.0; }, - Collider::Box { - radius, - z_min, - z_max, - } => { - // FIXME: - // Deprecated, should remove? - // - // 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 { z_min, z_max, @@ -894,7 +851,7 @@ impl<'a> PhysicsData<'a> { radius: _, } => { // Scale collider - let radius = collider.get_radius().min(0.45) * scale; + let radius = collider.bounding_radius().min(0.45) * scale; let z_min = *z_min * scale; let z_max = z_max.clamped(1.2, 1.95) * scale; @@ -1039,7 +996,7 @@ impl<'a> PhysicsData<'a> { let entity_center = pos.0 + (z_limits.0 + half_height) * Vec3::unit_z(); let path_center = entity_center + pos_delta / 2.0; - let flat_radius = collider.get_radius() * scale; + let flat_radius = collider.bounding_radius() * scale; let radius = (flat_radius.powi(2) + half_height.powi(2)).sqrt(); let path_bounding_radius = radius + (pos_delta / 2.0).magnitude(); @@ -1087,7 +1044,7 @@ impl<'a> PhysicsData<'a> { // use bounding cylinder regardless of our collider // TODO: extract point-terrain collision above to its own // function - let radius = collider.get_radius(); + let radius = collider.bounding_radius(); let (z_min, z_max) = collider.get_z_limits(1.0); let radius = radius.min(0.45) * scale; @@ -1301,7 +1258,7 @@ impl<'a> PhysicsData<'a> { .for_each(|(entity, pos, scale, collider)| { let scale = scale.map(|s| s.0).unwrap_or(1.0); let radius_2d = - (collider.map(|c| c.get_radius()).unwrap_or(0.5) * scale).ceil() as u32; + (collider.map(|c| c.bounding_radius()).unwrap_or(0.5) * scale).ceil() as u32; let pos_2d = pos.0.xy().map(|e| e as i32); const POS_TRUNCATION_ERROR: u32 = 1; spatial_grid.insert(pos_2d, radius_2d + POS_TRUNCATION_ERROR, entity); @@ -1801,8 +1758,9 @@ fn voxel_collider_bounding_sphere( } } +/// Returns whether interesction between entities occured #[allow(clippy::too_many_arguments)] -fn try_e2e_collision( +fn resolve_e2e_collision( // utility variables for our entity collision_registered: &mut bool, entity_entity_collisions: &mut u64, @@ -1828,7 +1786,7 @@ fn try_e2e_collision( collider_other: Option<&Collider>, mass: Mass, mass_other: Mass, -) -> ControlFlow<()> { +) -> bool { // Find the distance betwen our collider and // collider we collide with and get vector of pushback. // @@ -1850,7 +1808,7 @@ fn try_e2e_collision( let in_z_range = ceiling >= floor_other && floor <= ceiling_other; if !in_z_range { - return ControlFlow::Continue(()); + return false; } let ours = ColliderContext { @@ -1865,7 +1823,7 @@ fn try_e2e_collision( let in_collision_range = diff.magnitude_squared() <= collision_dist.powi(2); if !in_collision_range { - return ControlFlow::Continue(()); + return false; } // If entities have not yet collided this tick (but just did) and if entity @@ -1900,16 +1858,17 @@ fn try_e2e_collision( && !matches!(collider_other, Some(Collider::Voxel { .. })) && !matches!(collider, Some(Collider::Voxel { .. })) { + const ELASTIC_FORCE_COEFFICIENT: f32 = 400.0; let mass_coefficient = mass_other.0 / (mass.0 + mass_other.0); let distance_coefficient = collision_dist - diff.magnitude(); - let force = 400.0 * distance_coefficient * mass_coefficient; + let force = ELASTIC_FORCE_COEFFICIENT * distance_coefficient * mass_coefficient; *vel_delta += Vec3::from(diff.normalized()) * force * step_delta; } *collision_registered = true; - ControlFlow::Continue(()) + true } struct ColliderContext<'a> { @@ -1958,17 +1917,32 @@ fn projection_between(c0: ColliderContext, c1: ColliderContext) -> (Vec2, f } } +/// Returns the points on line segments n and m respectively that are the +/// closest to one-another. If the lines are parallel, an arbitrary, +/// unspecified pair of points that sit on the line segments will be chosen. fn closest_points(n: LineSegment2, m: LineSegment2) -> (Vec2, Vec2) { - let p0 = n.start; - let r1 = n.end - n.start; - let q0 = m.start; - let r2 = m.end - m.start; + // TODO: Rewrite this to something reasonable, if you have faith + #![allow(clippy::many_single_char_names)] - // The solution for following equations - // t = (q0.x + u * r2.x - p0.x) / r1.x - // u = (p0.y + t * r1.y - q0.y) / r2.y - let t = (r2.y / r2.x * (q0.x - p0.x) + p0.y - q0.y) / (r1.x * r2.y / r2.x - r1.y); - let u = (p0.y + t * r1.y - q0.y) / r2.y; + let a = n.start; + let b = n.end - n.start; + let c = m.start; + let d = m.end - m.start; + + // Check to prevent div by 0.0 (produces NaNs) and minimize precision + // loss from dividing by small values. + // If both d.x and d.y are 0.0 then the segment is a point and we are fine + // to fallback to the end point projection. + let t = if d.x > d.y { + (d.y / d.x * (c.x - a.x) + a.y - c.y) / (b.x * d.y / d.x - b.y) + } else { + (d.x / d.y * (c.y - a.y) + a.x - c.x) / (b.y * d.x / d.y - b.x) + }; + let u = if d.y > d.x { + (a.y + t * b.y - c.y) / d.y + } else { + (a.x + t * b.x - c.x) / d.x + }; // Check to see whether the lines are parallel if !t.is_finite() || !u.is_finite() { @@ -1978,14 +1952,14 @@ fn closest_points(n: LineSegment2, m: LineSegment2) -> (Vec2, Vec (n.start, m.projected_point(n.start)), (n.end, m.projected_point(n.end)), ]) - .min_by_key(|(a, b)| ordered_float::OrderedFloat(a.distance_squared(*b))) - .expect("Lines had non-finite elements") + .min_by_key(|(a, b)| ordered_float::OrderedFloat(a.distance_squared(*b))) + .expect("Lines had non-finite elements") } else { let t = t.clamped(0.0, 1.0); let u = u.clamped(0.0, 1.0); - let close_n = p0 + r1 * t; - let close_m = q0 + r2 * u; + let close_n = a + b * t; + let close_m = c + d * u; let proj_n = n.projected_point(close_m); let proj_m = m.projected_point(close_n); diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index 0802a3f3e9..ea791d163a 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -203,20 +203,7 @@ impl StateExt for State { comp::Body::Ship(ship) => comp::Collider::Voxel { id: ship.manifest_entry().to_string(), }, - _ => { - 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(), - } - }, + _ => capsule(&body), }) .with(comp::Controller::default()) .with(body) @@ -248,11 +235,7 @@ impl StateExt for State { .with(comp::Ori::default()) .with(body.mass()) .with(body.density()) - .with(comp::Collider::Box { - radius: body.max_radius(), - z_min: 0.0, - z_max: body.height(), - }) + .with(capsule(&body)) .with(body) } @@ -314,11 +297,7 @@ impl StateExt for State { if projectile.is_point { projectile_base = projectile_base.with(comp::Collider::Point) } else { - projectile_base = projectile_base.with(comp::Collider::Box { - radius: body.max_radius(), - z_min: 0.0, - z_max: body.height(), - }) + projectile_base = projectile_base.with(capsule(&body)) } projectile_base.with(projectile).with(body) @@ -391,11 +370,7 @@ impl StateExt for State { .with(pos) .with(comp::Vel(Vec3::zero())) .with(comp::Ori::default()) - .with(comp::Collider::Box { - radius: comp::Body::Object(object).max_radius(), - z_min: 0.0, - z_max: comp::Body::Object(object).height() - }) + .with(capsule(&object.into())) .with(comp::Body::Object(object)) .with(comp::Mass(10.0)) // .with(comp::Sticky) @@ -467,7 +442,9 @@ impl StateExt for State { self.write_component_ignore_entity_dead(entity, comp::Pos(spawn_point)); self.write_component_ignore_entity_dead(entity, comp::Vel(Vec3::zero())); self.write_component_ignore_entity_dead(entity, comp::Ori::default()); - self.write_component_ignore_entity_dead(entity, comp::Collider::Box { + self.write_component_ignore_entity_dead(entity, comp::Collider::CapsulePrism { + p0: Vec2::zero(), + p1: Vec2::zero(), radius: 0.4, z_min: 0.0, z_max: 1.75, @@ -509,11 +486,7 @@ impl StateExt for State { // and we call nothing that can delete it in any of the subsequent // commands, so we can assume that all of these calls succeed, // justifying ignoring the result of insertion. - self.write_component_ignore_entity_dead(entity, comp::Collider::Box { - radius: body.max_radius(), - z_min: 0.0, - z_max: body.height(), - }); + self.write_component_ignore_entity_dead(entity, capsule(&body)); self.write_component_ignore_entity_dead(entity, body); self.write_component_ignore_entity_dead(entity, body.mass()); self.write_component_ignore_entity_dead(entity, body.density()); @@ -896,3 +869,15 @@ fn send_to_group(g: &comp::Group, ecs: &specs::World, msg: &comp::ChatMsg) { } } } + +fn capsule(body: &comp::Body) -> comp::Collider { + let (p0, p1, radius) = body.sausage(); + + comp::Collider::CapsulePrism { + p0, + p1, + radius, + z_min: 0.0, + z_max: body.height(), + } +} diff --git a/voxygen/src/scene/mod.rs b/voxygen/src/scene/mod.rs index 8d41883e28..9d17c29eab 100644 --- a/voxygen/src/scene/mod.rs +++ b/voxygen/src/scene/mod.rs @@ -1164,30 +1164,6 @@ impl Scene { .join() { match collider { - comp::Collider::Box { - radius, - z_min, - z_max, - } => { - current_entities.insert(entity); - let shape_id = hitboxes.entry(entity).or_insert_with(|| { - self.debug.add_shape(DebugShape::Cylinder { - 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] - }; - // 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, From e5d69d153b56057b7fbbbb27047aa54bebd0ba3f Mon Sep 17 00:00:00 2001 From: juliancoffee Date: Thu, 16 Sep 2021 20:47:34 +0300 Subject: [PATCH 12/13] Adress review Make sausage ascii-art have `a` line inside of stadium --- common/src/comp/body.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/common/src/comp/body.rs b/common/src/comp/body.rs index 291eeee15e..946b170df8 100644 --- a/common/src/comp/body.rs +++ b/common/src/comp/body.rs @@ -422,13 +422,13 @@ impl Body { // // xxxxxxxxxxxxxxxxx // - // _ aaaaaaaaa - // y -* r * - + // _ ----------_ + // y -* r *- // y * r * - // y * rrr --------- rrr * + // y * rrr aaaaaaaaa 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); From 19f0cf4ee591ca8eb5b46a392a708a1ec1f88411 Mon Sep 17 00:00:00 2001 From: juliancoffee Date: Thu, 16 Sep 2021 21:59:52 +0300 Subject: [PATCH 13/13] Set z_min to 0 for terrain collision checks This allows us have different z_min for e2e checks --- common/systems/src/phys.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/common/systems/src/phys.rs b/common/systems/src/phys.rs index 4cf7ea8beb..525d39beba 100644 --- a/common/systems/src/phys.rs +++ b/common/systems/src/phys.rs @@ -821,7 +821,8 @@ impl<'a> PhysicsData<'a> { // Additionally, multiply radius by 0.1 to make // the cylinder smaller to avoid lag. let radius = collider.bounding_radius() * scale * 0.1; - let (z_min, z_max) = collider.get_z_limits(scale); + let (_, z_max) = collider.get_z_limits(scale); + let z_min = 0.0; let mut cpos = *pos; let cylinder = (radius, z_min, z_max); @@ -844,7 +845,7 @@ impl<'a> PhysicsData<'a> { tgt_pos = cpos.0; }, Collider::CapsulePrism { - z_min, + z_min: _, z_max, p0: _, p1: _, @@ -852,7 +853,7 @@ impl<'a> PhysicsData<'a> { } => { // Scale collider let radius = collider.bounding_radius().min(0.45) * scale; - let z_min = *z_min * scale; + let z_min = 0.0; let z_max = z_max.clamped(1.2, 1.95) * scale; let cylinder = (radius, z_min, z_max); @@ -1045,10 +1046,10 @@ impl<'a> PhysicsData<'a> { // TODO: extract point-terrain collision above to its own // function let radius = collider.bounding_radius(); - let (z_min, z_max) = collider.get_z_limits(1.0); + let (_, z_max) = collider.get_z_limits(1.0); let radius = radius.min(0.45) * scale; - let z_min = z_min * scale; + let z_min = 0.0; let z_max = z_max.clamped(1.2, 1.95) * scale; if let Some(voxel_collider) =