dimforge / parry

2D and 3D collision-detection library for Rust.
https://parry.rs
Apache License 2.0
594 stars 104 forks source link

Shape casting returns incorrect normals #193

Open sweglord227 opened 7 months ago

sweglord227 commented 7 months ago

Coming from bevy_xpbd, normals are very imprecise when using shape casts via bevy_xpbd's spacial query's shape_hits function. i am creating a kinematic character controller, and it gives incorrect values when moving almost orthogonal to colliders, even when they are completely aligned to grid. it isn't by a ton, but it's enough to clip through colliders. after discussing it with the creator of bevy_xpbd and a mod of the bevy discord, i was told it was likely a parry issue and to report it here.

i've also noticed that normals are almost never returned normalized. there tends to be a bit of fluff in one axis or the other, even when colliding head on.

if you need anything else from me, please let me know.

in the image i was clipping into a 10x5x0.1 (bevy coordinates) meter wall aligned to grid. collisions work fine when pushing directly against the wall, but fail when moving into it at a shallow angle.

image

HeartofPhos commented 1 day ago

I've created some examples where normals behave strangely In all cases a target shape is intersecting slightly with the Y+ face of a cuboid, but a minor difference in penetration results in very different normals

In this case I believe the intended functionality is to always have N2 be [0.0, 1.0, 0.0] (with some floating point imprecision)

Tested on version 0.17.4

use parry3d::{
    math::{Isometry, Vector},
    query::{self, ShapeCastOptions},
    shape::{Ball, Capsule, Cuboid, Shape},
};

fn main() {
    let cases: [fn() -> (Box<dyn Shape>, Isometry<f32>); 4] = [
        // broken
        // N1 [[0.7068505, 0.026921066, 0.7068505]]
        // N2 [[-0.7068505, -0.026921066, -0.7068505]]
        || {
            let g = Capsule::new_y(5.0, 1.0);
            let pos = Isometry::translation(0.0, g.half_height() - 0.01, 0.0);

            (Box::new(g), pos)
        },
        // working
        // N1 [[4.7682978e-8, -1.0, 4.7682978e-8]]
        // N2 [[-4.7682978e-8, 1.0, -4.7682978e-8]]
        || {
            let g = Capsule::new_y(5.0, 1.0);
            let pos = Isometry::translation(0.0, g.half_height() - 0.02, 0.0);

            (Box::new(g), pos)
        },
        // broken
        // N1 [[-0.70708853, -0.007185684, -0.70708853]]
        // N2 [[0.70708853, 0.007185684, 0.70708853]]
        || {
            let g = Ball::new(1.0);
            let pos = Isometry::translation(0.0, g.radius - 0.2, 0.0);

            (Box::new(g), pos)
        },
        // working
        // N1 [[-0.0, -1.0, -0.0]]
        // N2 [[0.0, 1.0, 0.0]]
        || {
            let g = Ball::new(1.0);
            let pos = Isometry::translation(0.0, g.radius - 0.200001, 0.0);

            (Box::new(g), pos)
        },
    ];

    let (g1, pos1) = cases[0]();
    // velocity not being straight down seems to play a part
    let vel1 = Vector::new(0.1, 1.0, 0.0);

    let g2 = Cuboid::new(Vector::new(20.0, 1.0, 20.0));
    let pos2 = Isometry::translation(0.0, -g2.half_extents.y, 0.0);
    let vel2 = Vector::new(0.0, 0.0, 0.0);

    let options = ShapeCastOptions {
        compute_impact_geometry_on_penetration: true,
        ..Default::default()
    };

    let result = query::cast_shapes(&pos1, &vel1, &*g1, &pos2, &vel2, &g2, options);

    if let Some(result) = result.unwrap() {
        println!("// N1 {:?}", result.normal1);
        println!("// N2 {:?}", result.normal2);
        println!("{:?}", result.status);
    }
}