Merge branch 'juliancoffee/capsule_prism' into 'master'

New CapsulePrism collider

See merge request veloren/veloren!2843
This commit is contained in:
Joshua Barretto 2021-09-17 11:39:57 +00:00
commit 9cc70e6d22
20 changed files with 1098 additions and 515 deletions

View File

@ -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);
}

View File

@ -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<f32> {
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]: <https://en.wikipedia.org/wiki/Stadium_(geometry)>
pub fn sausage(&self) -> (Vec2<f32>, Vec2<f32>, f32) {
// Consider this ascii-art stadium with radius `r` and line segment `a`
//
// xxxxxxxxxxxxxxxxx
//
// _ ----------_
// 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 {

View File

@ -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<f32>, Vec2<f32>)>,
pub ori: Quaternion<f32>,
}
@ -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<f32>,
p1: Vec2<f32>,
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),
}
}

View File

@ -232,8 +232,9 @@ fn height_offset(body: &Body, look_dir: Dir) -> f32 {
}
pub fn beam_offsets(body: &Body, look_dir: Dir, ori: Vec3<f32>) -> Vec3<f32> {
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,

View File

@ -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),
},
));
}

View File

@ -641,7 +641,7 @@ pub fn handle_manipulate_loadout(
// MAX_PICKUP_RANGE and the radius of the body
let sprite_range_check = |pos: Vec3<f32>| {
(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

View File

@ -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)

View File

@ -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);

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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);

View File

@ -715,8 +715,8 @@ pub fn handle_explosion(server: &Server, pos: Vec3<f32>, explosion: Explosion, o
cyl_body: Body,
) -> f32 {
// 2d check
let horiz_dist =
Vec2::<f32>::from(sphere_pos - cyl_pos).distance(Vec2::default()) - cyl_body.radius();
let horiz_dist = Vec2::<f32>::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 =

View File

@ -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(),
}
}

View File

@ -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());

View File

@ -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<Consts<Locals>>;

View File

@ -9,12 +9,21 @@ use vek::*;
#[derive(Debug)]
pub enum DebugShape {
Line([Vec3<f32>; 2]),
Cylinder { radius: f32, height: f32 },
Cylinder {
radius: f32,
height: f32,
},
CapsulePrism {
p0: Vec2<f32>,
p1: Vec2<f32>,
radius: f32,
height: f32,
},
}
impl DebugShape {
pub fn mesh(&self) -> Mesh<DebugVertex> {
use core::f32::consts::PI;
use core::f32::consts::{PI, TAU};
let mut mesh = Mesh::new();
let tri = |x: Vec3<f32>, y: Vec3<f32>, z: Vec3<f32>| {
Tri::<DebugVertex>::new(x.into(), y.into(), z.into())
@ -22,28 +31,115 @@ impl DebugShape {
let quad = |x: Vec3<f32>, y: Vec3<f32>, z: Vec3<f32>, w: Vec3<f32>| {
Quad::<DebugVertex>::new(x.into(), y.into(), z.into(), w.into())
};
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<DebugVertex>, origin: Vec3<f32>, 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<DebugShapeId, DebugShape>,
pending_locals: HashMap<DebugShapeId, ([f32; 4], [f32; 4])>,
#[allow(clippy::type_complexity)]
pending_locals: HashMap<DebugShapeId, ([f32; 4], [f32; 4], [f32; 4])>,
pending_deletes: HashSet<DebugShapeId>,
models: HashMap<DebugShapeId, (Model<DebugVertex>, Bound<Consts<DebugLocals>>)>,
}
@ -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 {

View File

@ -1152,32 +1152,49 @@ impl Scene {
if settings.interface.toggle_hitboxes {
let positions = ecs.read_component::<comp::Pos>();
let colliders = ecs.read_component::<comp::Collider>();
let orientations = ecs.read_component::<comp::Ori>();
let groups = ecs.read_component::<comp::Group>();
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
},
}
}
}

View File

@ -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::<f32>();
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::<f32>();
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::<f32>::zero()

View File

@ -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 {

View File

@ -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);
},
})
}