Skip to content

Reduce operations in RayCast2d::circle_intersection_at using cross product#19103

Merged
alice-i-cecile merged 2 commits intobevyengine:mainfrom
kumikaya:opti_circle_intersection_at
May 26, 2025
Merged

Reduce operations in RayCast2d::circle_intersection_at using cross product#19103
alice-i-cecile merged 2 commits intobevyengine:mainfrom
kumikaya:opti_circle_intersection_at

Conversation

@kumikaya
Copy link
Contributor

@kumikaya kumikaya commented May 6, 2025

Objective

  • Using the cross product to compute the perpendicular distance reduces one multiplication and two additions.

@kumikaya
Copy link
Contributor Author

kumikaya commented May 6, 2025

I conducted a simple performance benchmark on my laptop:

use bevy_math::{
    bounding::{BoundingCircle, RayCast2d},
    ops, Dir2, FloatPow, Vec2,
};
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use rand::Rng;

pub trait RayCast2dExt {
    /// Get the distance of an intersection with a [`BoundingCircle`], if any.
    fn circle_intersection_at_new(&self, circle: &BoundingCircle) -> Option<f32>;
}

impl RayCast2dExt for RayCast2d {
    fn circle_intersection_at_new(&self, circle: &BoundingCircle) -> Option<f32> {
        let offset = self.ray.origin - circle.center;
        let projected = offset.dot(*self.ray.direction);
        let normal_dist = offset.x * self.ray.direction.y - offset.y * self.ray.direction.x;
        let distance_squared = circle.radius().squared() - normal_dist.squared();
        if distance_squared < 0.
            || ops::copysign(projected.squared(), -projected) < -distance_squared
        {
            None
        } else {
            let toi = -projected - ops::sqrt(distance_squared);
            if toi > self.max {
                None
            } else {
                Some(toi.max(0.))
            }
        }
    }
}

fn generate_rays(n: usize) -> Vec<RayCast2d> {
    let mut rays = Vec::with_capacity(n);
    let mut rng = rand::rng();
    for _ in 0..n {
        let origin = Vec2::new(
            rng.random_range(-1000.0..1000.0),
            rng.random_range(-1000.0..1000.0),
        );
        let dir = Vec2::new(rng.random_range(-1.0..1.0), rng.random_range(-1.0..1.0));
        if let Ok(dir2) = Dir2::new(dir) {
            rays.push(RayCast2d::new(origin, dir2, 10000.0));
        }
    }
    rays
}

fn bench_intersection(c: &mut Criterion) {
    let rays = generate_rays(100_000);
    let circle = BoundingCircle::new(Vec2::ZERO, 1.5);

    c.bench_function("circle_intersection_at", |b| {
        b.iter(|| {
            let mut sum = 0.0;
            for ray in &rays {
                if let Some(d) = black_box(ray).circle_intersection_at(black_box(&circle)) {
                    sum += d;
                }
            }
            black_box(sum);
        })
    });

    c.bench_function("circle_intersection_at_new", |b| {
        b.iter(|| {
            let mut sum = 0.0;
            for ray in &rays {
                if let Some(d) = black_box(ray).circle_intersection_at_new(black_box(&circle)) {
                    sum += d;
                }
            }
            black_box(sum);
        })
    });
}

criterion_group!(benches, bench_intersection);
criterion_main!(benches);

The results are as follows:

Gnuplot not found, using plotters backend
circle_intersection_at  time:   [310.15 µs 320.96 µs 332.61 µs]
                        change: [-9.9764% -5.0982% -0.5121%] (p = 0.04 < 0.05)
                        Change within noise threshold.
Found 4 outliers among 100 measurements (4.00%)
  1 (1.00%) high mild
  3 (3.00%) high severe

circle_intersection_at_new
                        time:   [174.53 µs 176.12 µs 177.92 µs]
                        change: [-9.3318% -6.9158% -4.5095%] (p = 0.00 < 0.05)
                        Performance has improved.
Found 5 outliers among 100 measurements (5.00%)
  3 (3.00%) high mild
  2 (2.00%) high severe

@mockersf mockersf added A-Math Fundamental domain-agnostic mathematical operations S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels May 6, 2025
Copy link
Contributor

@IQuick143 IQuick143 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice find.

@IQuick143 IQuick143 added S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it D-Straightforward Simple bug fixes and API improvements, docs, test and examples and removed S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels May 11, 2025
@alice-i-cecile alice-i-cecile added this pull request to the merge queue May 26, 2025
Merged via the queue into bevyengine:main with commit ee3d9b2 May 26, 2025
40 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-Math Fundamental domain-agnostic mathematical operations D-Straightforward Simple bug fixes and API improvements, docs, test and examples S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants