bevy_xpbd icon indicating copy to clipboard operation
bevy_xpbd copied to clipboard

avian2d reports collisions between non-intersecting `HalfSpace` and `Rectangle`

Open tgiannak opened this issue 11 months ago • 2 comments

avian2d (including release v0.1.2 and e23d070) reports collisions between non-intersecting (and in fact, far apart) half-space and rectangle (as well as other) colliders.

I believe this is because for most other collider pairs, parry2d reports invalid normals while computing the contact manifolds, and that is what Avian uses to filter out contact manifolds that do not include contacts.

However, even when there are no contacts (i.e., manifold.points.is_empty() == true), parry2d still reports valid normals for collider pairs such as half-space and rectangle. I have also seen this behavior with half-space and segment.

The parry2d documentation isn't sufficient for me to easily determine whether additionally (or only) filtering based on reported contact points is the correct way to fix the false collision reports.

The following code demonstrates the bug in Avian. Since the results of contact_manifolds are used to populate the list of collisions with no further processing (they go from here to here to here), this results in collisions being reported for the rectangle but not the circle. In this case, neither should have collisions reported.

use avian2d::{collision::contact_query::contact_manifolds, math::Vector2, prelude::Collider};

fn main() {
    let bottom_half = Collider::half_space(Vector2::Y);
    let ball = Collider::circle(10.0);
    let rect = Collider::rectangle(10.0, 10.0);

    let center = Vector2::new(0.0, 0.0);
    let above_center = Vector2::new(0.0, 500.0);

    {
        let manifolds = contact_manifolds(&bottom_half, center, 0.0, &ball, above_center, 0.0, 0.0);
        assert_eq!(manifolds.len(), 0);
    }

    {
        let manifolds = contact_manifolds(&bottom_half, center, 0.0, &rect, above_center, 0.0, 0.0);
        assert_eq!(manifolds.len(), 1);
    }
}

The following code demonstrates the behavior of parry2d.

use parry2d::{
    na::{Isometry2, Vector2},
    query::{
        intersection_test, ContactManifold, DefaultQueryDispatcher, PersistentQueryDispatcher,
    },
    shape::{Ball, Cuboid, HalfSpace},
};

fn main() {
    // A half space covering the bottom half of the screen. (The normal points to the uncovered part.)
    let bottom_space = HalfSpace::new(Vector2::y_axis());

    // A half space covering the top half of the screen. (The normal points to the uncovered part.)
    let top_space = HalfSpace::new(-Vector2::y_axis());

    // Some small shapes.
    let rect = Cuboid::new(Vector2::new(10.0, 10.0));
    let ball = Ball::new(10.0);

    let above_center = Isometry2::translation(0.0, 500.0);
    let center = Isometry2::identity();

    // Confirm that the direction of the half-spaces.
    assert!(!intersection_test(&center, &bottom_space, &above_center, &rect).unwrap());
    assert!(intersection_test(&center, &top_space, &above_center, &rect).unwrap());

    {
        // Put the ball in the top half with the bottom-half-space and there are
        // no collision points and the normals are not valid.
        let mut manifolds: Vec<ContactManifold<(), ()>> = vec![];

        let result = DefaultQueryDispatcher.contact_manifolds(
            &above_center,
            &bottom_space,
            &ball,
            0.0,
            &mut manifolds,
            &mut None,
        );

        assert!(result.is_ok());
        assert_eq!(manifolds.len(), 1);
        assert_eq!(manifolds[0].points.len(), 0);
        assert_eq!(manifolds[0].local_n1, Vector2::new(0.0, 0.0));
        assert_eq!(manifolds[0].local_n2, Vector2::new(0.0, 0.0));
    }

    {
        // Put the ball in the top half with the top-half-space and there are
        // collision points and the normals are valid.
        let mut manifolds: Vec<ContactManifold<(), ()>> = vec![];

        let result = DefaultQueryDispatcher.contact_manifolds(
            &above_center,
            &top_space,
            &ball,
            0.0,
            &mut manifolds,
            &mut None,
        );

        assert!(result.is_ok());
        assert_eq!(manifolds.len(), 1);
        assert_ne!(manifolds[0].points.len(), 0);
        assert_eq!(manifolds[0].local_n1, Vector2::new(0.0, -1.0));
        assert_eq!(manifolds[0].local_n2, Vector2::new(0.0, 1.0));
    }

    {
        // Put the ball and rect near each other and confirm that there are no
        // collision points and the norms are not valid.
        let mut manifolds: Vec<ContactManifold<(), ()>> = vec![];
        let result = DefaultQueryDispatcher.contact_manifolds(
            &above_center,
            &rect,
            &ball,
            0.0,
            &mut manifolds,
            &mut None,
        );

        assert!(result.is_ok());
        assert_eq!(manifolds.len(), 1);
        assert_eq!(manifolds[0].points.len(), 0);
        assert_eq!(manifolds[0].local_n1, Vector2::new(0.0, 0.0));
        assert_eq!(manifolds[0].local_n2, Vector2::new(0.0, 0.0));
    }

    {
        // Put the rect in the top half with the bottom-half-space and there are
        // no collision points, but the normals *are* valid.
        let mut manifolds: Vec<ContactManifold<(), ()>> = vec![];
        let result = DefaultQueryDispatcher.contact_manifolds(
            &above_center,
            &bottom_space,
            &rect,
            0.0,
            &mut manifolds,
            &mut None,
        );

        assert!(result.is_ok());
        assert_eq!(manifolds.len(), 1);
        assert_eq!(manifolds[0].points.len(), 0);
        assert_eq!(manifolds[0].local_n1, Vector2::new(0.0, 1.0));
        assert_eq!(manifolds[0].local_n2, Vector2::new(0.0, -1.0));
    }
}

tgiannak avatar Dec 09 '24 03:12 tgiannak

Also happens with 3D (bevy 0.15, avian3d b47c30c5ef)

use avian3d::prelude::*;
use bevy::{
    app::{App, Startup},
    prelude::*,
    DefaultPlugins,
};

#[derive(Component)]
pub struct Cube;

#[derive(Component)]
pub struct Floor;

fn main() {
    App::new()
        .add_plugins((
            DefaultPlugins,
            PhysicsPlugins::default(),
            PhysicsDebugPlugin::default(),
        ))
        .add_systems(Startup, setup)
        .add_systems(PostProcessCollisions, log_collisions.run_if(run_once))
        .run();
}

pub fn setup(mut commands: Commands) {
    commands.spawn((Cube, RigidBody::Dynamic, Collider::cuboid(1.0, 1.0, 1.0)));
    commands.spawn((
        Floor,
        RigidBody::Static,
        Transform::from_xyz(0., -2.0, 0.),
        Collider::half_space(Vec3::Y),
    ));
}

fn log_collisions(
    collisions: Res<Collisions>,
    floor: Single<Entity, With<Floor>>,
    cube: Single<Entity, With<Cube>>,
) {
    if let Some(contacts) = collisions.get(*floor, *cube) {
        dbg!(&contacts);
    }
}

This logs the contact with empty contacts arrays:

[src/main.rs:42:9] &contacts = Contacts {
    entity1: 15v1#4294967311,
    entity2: 14v1#4294967310,
    body_entity1: Some(
        15v1#4294967311,
    ),
    body_entity2: Some(
        14v1#4294967310,
    ),
    manifolds: [
        ContactManifold {
            contacts: [],
            normal1: Vec3(
                0.0,
                1.0,
                0.0,
            ),
            normal2: Vec3(
                0.0,
                -1.0,
                0.0,
            ),
            index: 0,
        },
    ],
    is_sensor: false,
    during_current_frame: true,
    during_previous_frame: false,
    total_normal_impulse: 0.0,
    total_tangent_impulse: Vec2(
        0.0,
        0.0,
    ),
}

rectalogic avatar Dec 19 '24 23:12 rectalogic

I've just run into this as well.

whoistobias avatar Mar 06 '25 18:03 whoistobias

From what I can tell, e7d1a27611ab77f089991eece5b05ecf6e1d8d8b has fixed this by skipping empty manifolds.

tgiannak avatar Jul 24 '25 15:07 tgiannak

Closing as fixed as per the above, but feel free to reopen if this is still an issue :)

Jondolf avatar Jul 26 '25 23:07 Jondolf