fix(physics): resolve the earliest substep contact
Return exact path progress with each fixed-point line and circle candidate. Evaluate the complete recovered physical object set and apply the nearest contact, matching the selection logic in the original collision dispatcher instead of stopping at the lowest matching object id. This prevents dense assemblies from choosing a later wall or circle merely because its record appears earlier in the table. Test Plan: - `cargo test --all-targets` -- 45 passed - `cargo clippy --all-targets -- -D warnings` -- passed - `cargo build --profile production` -- passed - collision-progress ordering test -- passed - `git diff --cached --check` -- passed
This commit is contained in:
@@ -14,6 +14,12 @@ pub struct MilliVec {
|
||||
pub y: i32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct CollisionResponse {
|
||||
pub progress: f64,
|
||||
pub velocity: MilliVec,
|
||||
}
|
||||
|
||||
impl MilliVec {
|
||||
pub fn from_position(position: Vec2) -> Self {
|
||||
Self {
|
||||
@@ -76,21 +82,21 @@ const fn subtract(left: MilliVec, right: MilliVec) -> MilliVec {
|
||||
/// Reports whether the ball-center path crosses the registered line segment.
|
||||
/// A contact at the old position is excluded, matching the shooter-stop path:
|
||||
/// the waiting ball starts on object 25 and must be able to launch away from it.
|
||||
fn paths_intersect(
|
||||
fn path_intersection_progress(
|
||||
old_position: MilliVec,
|
||||
velocity: MilliVec,
|
||||
line_start: MilliVec,
|
||||
line_end: MilliVec,
|
||||
) -> bool {
|
||||
) -> Option<f64> {
|
||||
let line = subtract(line_end, line_start);
|
||||
let from_ball = subtract(line_start, old_position);
|
||||
let denominator = cross(velocity, line);
|
||||
if denominator == 0 {
|
||||
return false;
|
||||
return None;
|
||||
}
|
||||
let path_numerator = cross(from_ball, line);
|
||||
let line_numerator = cross(from_ball, velocity);
|
||||
if denominator > 0 {
|
||||
let intersects = if denominator > 0 {
|
||||
path_numerator > 0
|
||||
&& path_numerator <= denominator
|
||||
&& line_numerator >= 0
|
||||
@@ -100,29 +106,28 @@ fn paths_intersect(
|
||||
&& path_numerator >= denominator
|
||||
&& line_numerator <= 0
|
||||
&& line_numerator >= denominator
|
||||
}
|
||||
};
|
||||
intersects.then(|| path_numerator as f64 / denominator as f64)
|
||||
}
|
||||
|
||||
/// Apply the original type-2 response in the registered segment's basis.
|
||||
pub fn collide_with_line(
|
||||
/// Calculate the original type-2 response in the registered segment's basis.
|
||||
pub fn line_collision_response(
|
||||
old_position: MilliVec,
|
||||
velocity: &mut MilliVec,
|
||||
velocity: MilliVec,
|
||||
line_start: Vec2,
|
||||
line_end: Vec2,
|
||||
normal_rebound: f64,
|
||||
tangent_coupling: f64,
|
||||
) -> bool {
|
||||
) -> Option<CollisionResponse> {
|
||||
let start = MilliVec::from_position(line_start);
|
||||
let end = MilliVec::from_position(line_end);
|
||||
if !paths_intersect(old_position, *velocity, start, end) {
|
||||
return false;
|
||||
}
|
||||
let progress = path_intersection_progress(old_position, velocity, start, end)?;
|
||||
|
||||
let line_x = f64::from(end.x - start.x);
|
||||
let line_y = f64::from(end.y - start.y);
|
||||
let length = line_x.hypot(line_y);
|
||||
if length == 0.0 {
|
||||
return false;
|
||||
return None;
|
||||
}
|
||||
let tangent_x = line_x / length;
|
||||
let tangent_y = line_y / length;
|
||||
@@ -132,18 +137,110 @@ pub fn collide_with_line(
|
||||
let incoming_y = f64::from(velocity.y);
|
||||
let normal_speed = incoming_x * normal_x + incoming_y * normal_y;
|
||||
if normal_speed <= 0.0 {
|
||||
return false;
|
||||
return None;
|
||||
}
|
||||
let tangent_speed = incoming_x * tangent_x + incoming_y * tangent_y;
|
||||
let outgoing_normal = -normal_rebound * normal_speed;
|
||||
let outgoing_tangent = tangent_speed + tangent_coupling * normal_speed;
|
||||
|
||||
velocity.x = (normal_x * outgoing_normal + tangent_x * outgoing_tangent).round() as i32;
|
||||
velocity.y = (normal_y * outgoing_normal + tangent_y * outgoing_tangent).round() as i32;
|
||||
Some(CollisionResponse {
|
||||
progress,
|
||||
velocity: MilliVec {
|
||||
x: (normal_x * outgoing_normal + tangent_x * outgoing_tangent).round() as i32,
|
||||
y: (normal_y * outgoing_normal + tangent_y * outgoing_tangent).round() as i32,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn collide_with_line(
|
||||
old_position: MilliVec,
|
||||
velocity: &mut MilliVec,
|
||||
line_start: Vec2,
|
||||
line_end: Vec2,
|
||||
normal_rebound: f64,
|
||||
tangent_coupling: f64,
|
||||
) -> bool {
|
||||
let Some(response) = line_collision_response(
|
||||
old_position,
|
||||
*velocity,
|
||||
line_start,
|
||||
line_end,
|
||||
normal_rebound,
|
||||
tangent_coupling,
|
||||
) else {
|
||||
return false;
|
||||
};
|
||||
*velocity = response.velocity;
|
||||
true
|
||||
}
|
||||
|
||||
/// Apply the original type-1 circle response to a path entering the circle.
|
||||
/// Calculate the original type-1 circle response for a path entering it.
|
||||
pub fn circle_collision_response(
|
||||
old_position: MilliVec,
|
||||
velocity: MilliVec,
|
||||
center: Vec2,
|
||||
radius: f32,
|
||||
normal_rebound: f64,
|
||||
tangent_coupling: f64,
|
||||
normal_kick: f64,
|
||||
) -> Option<CollisionResponse> {
|
||||
let center = MilliVec::from_position(center);
|
||||
let radius_milli = (radius * 1_000.0).round() as i32;
|
||||
let from_center = subtract(old_position, center);
|
||||
let radius_squared = i64::from(radius_milli).pow(2);
|
||||
let old_distance_squared = i64::from(from_center.x).pow(2) + i64::from(from_center.y).pow(2);
|
||||
if old_distance_squared <= radius_squared {
|
||||
return None;
|
||||
}
|
||||
|
||||
let vx = f64::from(velocity.x);
|
||||
let vy = f64::from(velocity.y);
|
||||
let offset_x = f64::from(from_center.x);
|
||||
let offset_y = f64::from(from_center.y);
|
||||
let quadratic_a = vx * vx + vy * vy;
|
||||
if quadratic_a == 0.0 {
|
||||
return None;
|
||||
}
|
||||
let quadratic_b = 2.0 * (offset_x * vx + offset_y * vy);
|
||||
let quadratic_c = old_distance_squared as f64 - f64::from(radius_milli).powi(2);
|
||||
let discriminant = quadratic_b * quadratic_b - 4.0 * quadratic_a * quadratic_c;
|
||||
if discriminant < 0.0 {
|
||||
return None;
|
||||
}
|
||||
let progress = (-quadratic_b - discriminant.sqrt()) / (2.0 * quadratic_a);
|
||||
if !(0.0 < progress && progress <= 1.0) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let hit_x = offset_x + vx * progress;
|
||||
let hit_y = offset_y + vy * progress;
|
||||
let hit_length = hit_x.hypot(hit_y);
|
||||
if hit_length == 0.0 {
|
||||
return None;
|
||||
}
|
||||
let normal_x = hit_x / hit_length;
|
||||
let normal_y = hit_y / hit_length;
|
||||
let tangent_x = normal_y;
|
||||
let tangent_y = -normal_x;
|
||||
let normal_speed = vx * normal_x + vy * normal_y;
|
||||
if normal_speed >= 0.0 {
|
||||
return None;
|
||||
}
|
||||
let tangent_speed = vx * tangent_x + vy * tangent_y;
|
||||
let outgoing_normal =
|
||||
-normal_rebound * normal_speed + normal_kick * f64::from(MAXIMUM_SPEED_MILLI_PER_STEP);
|
||||
let outgoing_tangent = tangent_speed - tangent_coupling * normal_speed;
|
||||
Some(CollisionResponse {
|
||||
progress,
|
||||
velocity: MilliVec {
|
||||
x: (normal_x * outgoing_normal + tangent_x * outgoing_tangent).round() as i32,
|
||||
y: (normal_y * outgoing_normal + tangent_y * outgoing_tangent).round() as i32,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn collide_with_circle(
|
||||
old_position: MilliVec,
|
||||
velocity: &mut MilliVec,
|
||||
@@ -153,54 +250,18 @@ pub fn collide_with_circle(
|
||||
tangent_coupling: f64,
|
||||
normal_kick: f64,
|
||||
) -> bool {
|
||||
let center = MilliVec::from_position(center);
|
||||
let radius_milli = (radius * 1_000.0).round() as i32;
|
||||
let from_center = subtract(old_position, center);
|
||||
let radius_squared = i64::from(radius_milli).pow(2);
|
||||
let old_distance_squared = i64::from(from_center.x).pow(2) + i64::from(from_center.y).pow(2);
|
||||
if old_distance_squared <= radius_squared {
|
||||
let Some(response) = circle_collision_response(
|
||||
old_position,
|
||||
*velocity,
|
||||
center,
|
||||
radius,
|
||||
normal_rebound,
|
||||
tangent_coupling,
|
||||
normal_kick,
|
||||
) else {
|
||||
return false;
|
||||
}
|
||||
|
||||
let vx = f64::from(velocity.x);
|
||||
let vy = f64::from(velocity.y);
|
||||
let offset_x = f64::from(from_center.x);
|
||||
let offset_y = f64::from(from_center.y);
|
||||
let quadratic_a = vx * vx + vy * vy;
|
||||
if quadratic_a == 0.0 {
|
||||
return false;
|
||||
}
|
||||
let quadratic_b = 2.0 * (offset_x * vx + offset_y * vy);
|
||||
let quadratic_c = old_distance_squared as f64 - f64::from(radius_milli).powi(2);
|
||||
let discriminant = quadratic_b * quadratic_b - 4.0 * quadratic_a * quadratic_c;
|
||||
if discriminant < 0.0 {
|
||||
return false;
|
||||
}
|
||||
let progress = (-quadratic_b - discriminant.sqrt()) / (2.0 * quadratic_a);
|
||||
if !(0.0 < progress && progress <= 1.0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let hit_x = offset_x + vx * progress;
|
||||
let hit_y = offset_y + vy * progress;
|
||||
let hit_length = hit_x.hypot(hit_y);
|
||||
if hit_length == 0.0 {
|
||||
return false;
|
||||
}
|
||||
let normal_x = hit_x / hit_length;
|
||||
let normal_y = hit_y / hit_length;
|
||||
let tangent_x = normal_y;
|
||||
let tangent_y = -normal_x;
|
||||
let normal_speed = vx * normal_x + vy * normal_y;
|
||||
if normal_speed >= 0.0 {
|
||||
return false;
|
||||
}
|
||||
let tangent_speed = vx * tangent_x + vy * tangent_y;
|
||||
let outgoing_normal =
|
||||
-normal_rebound * normal_speed + normal_kick * f64::from(MAXIMUM_SPEED_MILLI_PER_STEP);
|
||||
let outgoing_tangent = tangent_speed - tangent_coupling * normal_speed;
|
||||
velocity.x = (normal_x * outgoing_normal + tangent_x * outgoing_tangent).round() as i32;
|
||||
velocity.y = (normal_y * outgoing_normal + tangent_y * outgoing_tangent).round() as i32;
|
||||
};
|
||||
*velocity = response.velocity;
|
||||
true
|
||||
}
|
||||
|
||||
@@ -335,6 +396,21 @@ mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collision_progress_orders_contacts_along_the_substep() {
|
||||
let old = MilliVec::default();
|
||||
let velocity = MilliVec { x: 10_000, y: 0 };
|
||||
let near =
|
||||
line_collision_response(old, velocity, vec2(2.0, 1.0), vec2(2.0, -1.0), 0.6, 0.1)
|
||||
.expect("near rail should be crossed");
|
||||
let far = line_collision_response(old, velocity, vec2(8.0, 1.0), vec2(8.0, -1.0), 0.6, 0.1)
|
||||
.expect("far rail should be crossed");
|
||||
|
||||
assert!((near.progress - 0.2).abs() < f64::EPSILON);
|
||||
assert!((far.progress - 0.8).abs() < f64::EPSILON);
|
||||
assert!(near.progress < far.progress);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ordinary_circle_matches_the_live_object_155_probe() {
|
||||
let old = MilliVec {
|
||||
|
||||
Reference in New Issue
Block a user