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..946b170df8 100644 --- a/common/src/comp/body.rs +++ b/common/src/comp/body.rs @@ -147,6 +147,14 @@ 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 + impl Body { pub fn is_humanoid(&self) -> bool { matches!(self, Body::Humanoid(_)) } @@ -292,11 +300,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 +359,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 { @@ -389,9 +400,60 @@ 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. + /// 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 + // + // _ ----------_ + // y -* r *- + // y * r * + // y * rrr aaaaaaaaa rrr * + // y * r * + // y * r * + // *____________ ^ + let dim = self.dimensions(); + // The width (shoulder to shoulder) and length (nose to tail) + let (width, length) = (dim.x, dim.y); + + if length > width { + // Dachshund-like + let radius = width / 2.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); + + (p0, p1, radius) + } else { + // Cyclops-like + let radius = length / 2.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); + + (p0, p1, radius) + } } // How far away other entities should try to be. Will be added uppon the other @@ -399,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, @@ -417,6 +479,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..99d68492d2 100644 --- a/common/src/comp/phys.rs +++ b/common/src/comp/phys.rs @@ -54,7 +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, + /// relative p0 and p1 of collider's statium, None if cylinder. + pub origins: Option<(Vec2, Vec2)>, pub ori: Quaternion, } @@ -99,18 +104,30 @@ 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, + }, + /// Capsule prism with line segment from p0 to p1 + CapsulePrism { + p0: Vec2, + p1: Vec2, + radius: f32, + z_min: f32, + z_max: f32, + }, Point, } 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 + }, Collider::Point => 0.0, } } @@ -123,7 +140,7 @@ 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/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/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/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 a83ba2ddce..525d39beba 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: None, 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(), @@ -213,20 +216,68 @@ 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::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 so that we can reduce - // the collision_boundary + // 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::Voxel { .. } | Collider::Point) | None => flat_radius, + }; + phys_cache.neighborhood_radius = neighborhood_radius; + + let ori = ori.to_quat(); + let origins = match collider { + Some(Collider::CapsulePrism { p0, p1, .. }) => { + let a = p1 - p0; + let len = a.magnitude(); + // 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 < std::f32::EPSILON * 10.0 { + 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(Collider::Voxel { .. } | Collider::Point) | None => None, + }; + phys_cache.origins = origins; + phys_cache.ori = ori; } } @@ -290,8 +341,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(), ) @@ -313,7 +365,7 @@ impl<'a> PhysicsData<'a> { mass, collider, sticky, - physics, + mut physics, projectile, char_state_maybe, )| { @@ -322,9 +374,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, @@ -347,22 +400,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( |( @@ -385,14 +436,13 @@ 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); entity_entity_collision_checks += 1; const MIN_COLLISION_DIST: f32 = 0.3; + let increments = ((previous_cache.velocity_dt - previous_cache_other.velocity_dt) .magnitude() @@ -401,82 +451,41 @@ impl<'a> PhysicsData<'a> { .ceil() as usize; let step_delta = 1.0 / increments as f32; + let mut collision_registered = false; for i in 0..increments { let factor = i as f32 * step_delta; - 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(); - - 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; - } - - // 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; - } + // 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, + factor, + &mut physics, + char_state_maybe, + &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, + ); } }, ); @@ -627,8 +636,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 +760,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 +787,22 @@ 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,13 +814,15 @@ 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 - let radius = collider.get_radius() * scale * 0.1; - let (z_min, z_max) = collider.get_z_limits(scale); + // 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.bounding_radius() * scale * 0.1; + let (_, z_max) = collider.get_z_limits(scale); + let z_min = 0.0; let mut cpos = *pos; let cylinder = (radius, z_min, z_max); @@ -827,14 +844,16 @@ impl<'a> PhysicsData<'a> { ); tgt_pos = cpos.0; }, - Collider::Box { - radius, - z_min, + Collider::CapsulePrism { + z_min: _, z_max, + p0: _, + p1: _, + radius: _, } => { // Scale collider - let radius = radius.min(0.45) * scale; - let z_min = *z_min * scale; + let radius = collider.bounding_radius().min(0.45) * scale; + let z_min = 0.0; let z_max = z_max.clamped(1.2, 1.95) * scale; let cylinder = (radius, z_min, z_max); @@ -866,8 +885,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 +903,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; @@ -975,7 +997,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(); @@ -987,6 +1009,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| { @@ -1022,11 +1045,11 @@ 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 (z_min, z_max) = collider.get_z_limits(1.0); + let radius = collider.bounding_radius(); + 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) = @@ -1236,7 +1259,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); @@ -1280,7 +1303,8 @@ impl<'a> System<'a> for Sys { } } -#[allow(clippy::too_many_arguments)] +#[warn(clippy::pedantic)] +#[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, @@ -1297,21 +1321,12 @@ 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(); + // 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 @@ -1327,9 +1342,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), @@ -1357,7 +1373,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,12 +1396,38 @@ 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) + .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(); + + let z_range = z_min..z_max; + physics_state.on_ground = None; physics_state.on_ceiling = false; 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; @@ -1395,55 +1436,62 @@ 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 { + 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 { @@ -1467,10 +1515,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); @@ -1480,48 +1527,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, block_true, 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, - block_true, - 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; @@ -1545,7 +1602,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(), @@ -1557,8 +1614,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 @@ -1637,28 +1693,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 { @@ -1698,3 +1758,281 @@ fn voxel_collider_bounding_sphere( radius, } } + +/// Returns whether interesction between entities occured +#[allow(clippy::too_many_arguments)] +fn resolve_e2e_collision( + // utility variables for our entity + collision_registered: &mut bool, + entity_entity_collisions: &mut u64, + factor: 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, +) -> bool { + // Find the distance betwen our collider and + // collider we collide with and get vector of pushback. + // + // If we aren't colliding, just skip step. + + // 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 (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 (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; + + if !in_z_range { + return false; + } + + 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 false; + } + + // 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 { .. })) + { + 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 = ELASTIC_FORCE_COEFFICIENT * distance_coefficient * mass_coefficient; + + *vel_delta += Vec3::from(diff.normalized()) * force * step_delta; + } + + *collision_registered = true; + + true +} + +struct ColliderContext<'a> { + pos: Vec3, + previous_cache: &'a PreviousPhysCache, +} + +/// 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) + } +} + +/// 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) { + // TODO: Rewrite this to something reasonable, if you have faith + #![allow(clippy::many_single_char_names)] + + 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() { + 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 = 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); + + 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 + // 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). + // 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: + 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 (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, + }; + + let projection = 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 (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) + } +} 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 d51463ae7e..ea791d163a 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -203,11 +203,7 @@ impl StateExt for State { comp::Body::Ship(ship) => comp::Collider::Voxel { id: ship.manifest_entry().to_string(), }, - _ => comp::Collider::Box { - radius: body.radius(), - z_min: 0.0, - z_max: body.height(), - }, + _ => capsule(&body), }) .with(comp::Controller::default()) .with(body) @@ -239,11 +235,7 @@ impl StateExt for State { .with(comp::Ori::default()) .with(body.mass()) .with(body.density()) - .with(comp::Collider::Box { - radius: body.radius(), - z_min: 0.0, - z_max: body.height(), - }) + .with(capsule(&body)) .with(body) } @@ -305,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.radius(), - z_min: 0.0, - z_max: body.height(), - }) + projectile_base = projectile_base.with(capsule(&body)) } projectile_base.with(projectile).with(body) @@ -382,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).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) @@ -458,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, @@ -500,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.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()); @@ -887,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/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/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..747e80c364 100644 --- a/voxygen/src/scene/debug.rs +++ b/voxygen/src/scene/debug.rs @@ -9,12 +9,21 @@ 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 { 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()) @@ -22,28 +31,115 @@ 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| { + let angle = TAU * 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| { + 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); + + 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 +151,8 @@ pub struct DebugShapeId(pub u64); pub struct Debug { next_shape_id: DebugShapeId, pending_shapes: HashMap, - pending_locals: HashMap, + #[allow(clippy::type_complexity)] + pending_locals: HashMap, pending_deletes: HashSet, models: HashMap, Bound>)>, } @@ -78,8 +175,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 +187,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 +197,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..9d17c29eab 100644 --- a/voxygen/src/scene/mod.rs +++ b/voxygen/src/scene/mod.rs @@ -1152,32 +1152,49 @@ 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::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/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 { 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); }, }) }