Skip to content
219 changes: 134 additions & 85 deletions benches/benches/bevy_picking/ray_mesh_intersection.rs
Original file line number Diff line number Diff line change
@@ -1,33 +1,53 @@
use core::hint::black_box;
use std::time::Duration;

use benches::bench;
use bevy_math::{Dir3, Mat4, Ray3d, Vec3};
use bevy_picking::mesh_picking::ray_cast;
use criterion::{criterion_group, Criterion};
use bevy_picking::mesh_picking::ray_cast::{self, Backfaces};
use criterion::{criterion_group, AxisScale, BenchmarkId, Criterion, PlotConfiguration};

fn ptoxznorm(p: u32, size: u32) -> (f32, f32) {
let ij = (p / (size), p % (size));
(ij.0 as f32 / size as f32, ij.1 as f32 / size as f32)
}
criterion_group!(benches, bench);

/// A mesh that can be passed to [`ray_cast::ray_mesh_intersection()`].
struct SimpleMesh {
positions: Vec<[f32; 3]>,
normals: Vec<[f32; 3]>,
indices: Vec<u32>,
}

fn mesh_creation(vertices_per_side: u32) -> SimpleMesh {
/// Selects a point within a normal square.
///
/// `p` is an index within `0..vertices_per_side.pow(2)`. The returned value is a coordinate where
/// both `x` and `z` are within `0..1`.
fn p_to_xz_norm(p: u32, vertices_per_side: u32) -> (f32, f32) {
let x = (p / vertices_per_side) as f32;
let z = (p % vertices_per_side) as f32;

let vertices_per_side = vertices_per_side as f32;

// Scale `x` and `z` to be between 0 and 1.
(x / vertices_per_side, z / vertices_per_side)
}

fn create_mesh(vertices_per_side: u32) -> SimpleMesh {
let mut positions = Vec::new();
let mut normals = Vec::new();
let mut indices = Vec::new();

for p in 0..vertices_per_side.pow(2) {
let xz = ptoxznorm(p, vertices_per_side);
positions.push([xz.0 - 0.5, 0.0, xz.1 - 0.5]);
let (x, z) = p_to_xz_norm(p, vertices_per_side);

// Push a new vertice to the mesh. We translate all vertices so the final square is
// centered at (0, 0), instead of (0.5, 0.5).
positions.push([x - 0.5, 0.0, z - 0.5]);

// All vertices have the same normal.
normals.push([0.0, 1.0, 0.0]);
}

let mut indices = vec![];
for p in 0..vertices_per_side.pow(2) {
if p % (vertices_per_side) != vertices_per_side - 1
&& p / (vertices_per_side) != vertices_per_side - 1
// Extend the indices for for all vertices except for the final row and column, since
// indices are "between" points.
if p % vertices_per_side != vertices_per_side - 1
&& p / vertices_per_side != vertices_per_side - 1
{
indices.extend_from_slice(&[p, p + 1, p + vertices_per_side]);
indices.extend_from_slice(&[p + vertices_per_side, p + 1, p + vertices_per_side + 1]);
Expand All @@ -41,81 +61,110 @@ fn mesh_creation(vertices_per_side: u32) -> SimpleMesh {
}
}

fn ray_mesh_intersection(c: &mut Criterion) {
let mut group = c.benchmark_group("ray_mesh_intersection");
group.warm_up_time(std::time::Duration::from_millis(500));

for vertices_per_side in [10_u32, 100, 1000] {
group.bench_function(format!("{}_vertices", vertices_per_side.pow(2)), |b| {
let ray = Ray3d::new(Vec3::new(0.0, 1.0, 0.0), Dir3::NEG_Y);
let mesh_to_world = Mat4::IDENTITY;
let mesh = mesh_creation(vertices_per_side);

b.iter(|| {
black_box(ray_cast::ray_mesh_intersection(
ray,
&mesh_to_world,
&mesh.positions,
Some(&mesh.normals),
Some(&mesh.indices),
ray_cast::Backfaces::Cull,
));
});
});
}
/// An enum that represents the configuration for all variations of the ray mesh intersection
/// benchmarks.
enum Benchmarks {
/// The ray intersects the mesh, and culling is enabled.
CullHit,

/// The ray intersects the mesh, and culling is disabled.
NoCullHit,

/// The ray does not intersect the mesh, and culling is enabled.
CullMiss,
}

fn ray_mesh_intersection_no_cull(c: &mut Criterion) {
let mut group = c.benchmark_group("ray_mesh_intersection_no_cull");
group.warm_up_time(std::time::Duration::from_millis(500));

for vertices_per_side in [10_u32, 100, 1000] {
group.bench_function(format!("{}_vertices", vertices_per_side.pow(2)), |b| {
let ray = Ray3d::new(Vec3::new(0.0, 1.0, 0.0), Dir3::NEG_Y);
let mesh_to_world = Mat4::IDENTITY;
let mesh = mesh_creation(vertices_per_side);

b.iter(|| {
black_box(ray_cast::ray_mesh_intersection(
ray,
&mesh_to_world,
&mesh.positions,
Some(&mesh.normals),
Some(&mesh.indices),
ray_cast::Backfaces::Include,
));
});
});
impl Benchmarks {
const WARM_UP_TIME: Duration = Duration::from_millis(500);
const VERTICES_PER_SIDE: [u32; 3] = [10, 100, 1000];

/// Returns an iterator over every variant in this enum.
fn iter() -> impl Iterator<Item = Self> {
[Self::CullHit, Self::NoCullHit, Self::CullMiss].into_iter()
}
}

fn ray_mesh_intersection_no_intersection(c: &mut Criterion) {
let mut group = c.benchmark_group("ray_mesh_intersection_no_intersection");
group.warm_up_time(std::time::Duration::from_millis(500));

for vertices_per_side in [10_u32, 100, 1000] {
group.bench_function(format!("{}_vertices", (vertices_per_side).pow(2)), |b| {
let ray = Ray3d::new(Vec3::new(0.0, 1.0, 0.0), Dir3::X);
let mesh_to_world = Mat4::IDENTITY;
let mesh = mesh_creation(vertices_per_side);

b.iter(|| {
black_box(ray_cast::ray_mesh_intersection(
ray,
&mesh_to_world,
&mesh.positions,
Some(&mesh.normals),
Some(&mesh.indices),
ray_cast::Backfaces::Cull,
));
});
});
/// Returns the benchmark group name.
fn name(&self) -> &'static str {
match *self {
Self::CullHit => bench!("cull_intersect"),
Self::NoCullHit => bench!("no_cull_intersect"),
Self::CullMiss => bench!("cull_no_intersect"),
}
}

fn ray(&self) -> Ray3d {
Ray3d::new(
Vec3::new(0.0, 1.0, 0.0),
match *self {
Self::CullHit | Self::NoCullHit => Dir3::NEG_Y,
// `NoIntersection` should not hit the mesh, so it goes an orthogonal direction.
Self::CullMiss => Dir3::X,
},
)
}

fn mesh_to_world(&self) -> Mat4 {
Mat4::IDENTITY
}

fn backface_culling(&self) -> Backfaces {
match *self {
Self::CullHit | Self::CullMiss => Backfaces::Cull,
Self::NoCullHit => Backfaces::Include,
}
}

/// Returns whether the ray should intersect with the mesh.
#[cfg(test)]
fn should_intersect(&self) -> bool {
match *self {
Self::CullHit | Self::NoCullHit => true,
Self::CullMiss => false,
}
}
}

criterion_group!(
benches,
ray_mesh_intersection,
ray_mesh_intersection_no_cull,
ray_mesh_intersection_no_intersection
);
/// A benchmark that times [`ray_cast::ray_mesh_intersection()`].
///
/// There are multiple different scenarios that are tracked, which are described by the
/// [`Benchmarks`] enum. Each scenario has its own benchmark group, where individual benchmarks
/// track a ray intersecting a square mesh of an increasing amount of vertices.
fn bench(c: &mut Criterion) {
for benchmark in Benchmarks::iter() {
let mut group = c.benchmark_group(benchmark.name());

group
.warm_up_time(Benchmarks::WARM_UP_TIME)
// Make the scale logarithmic, to match `VERTICES_PER_SIDE`.
.plot_config(PlotConfiguration::default().summary_scale(AxisScale::Logarithmic));

for vertices_per_side in Benchmarks::VERTICES_PER_SIDE {
group.bench_with_input(
BenchmarkId::from_parameter(format!("{}_vertices", vertices_per_side.pow(2))),
&vertices_per_side,
|b, &vertices_per_side| {
let ray = black_box(benchmark.ray());
let mesh_to_world = black_box(benchmark.mesh_to_world());
let mesh = black_box(create_mesh(vertices_per_side));
let backface_culling = black_box(benchmark.backface_culling());

b.iter(|| {
let intersected = ray_cast::ray_mesh_intersection(
ray,
&mesh_to_world,
&mesh.positions,
Some(&mesh.normals),
Some(&mesh.indices),
backface_culling,
);

#[cfg(test)]
assert_eq!(intersected.is_some(), benchmark.should_intersect());

intersected
});
},
);
}
}
}
Loading