Skip to content

Commit

Permalink
Example for random sampling (#13507)
Browse files Browse the repository at this point in the history
# Objective

We introduced a bunch of neat random sampling stuff in this release; we
should do a good job of showing people how to use it, and writing
examples is part of this.

## Solution

A new Math example, `random_sampling`, shows off the `ShapeSample` API
functionality. For the moment, it renders a cube and allows the user to
sample points from its interior or boundary in sets of either 1 or 100:
<img width="1440" alt="Screenshot 2024-05-25 at 1 16 08 PM"
src="https://github.com/bevyengine/bevy/assets/2975848/9cb6f53f-c89a-42c2-8907-b11d294c402a">

On the level of code, these are reflected by two ways of using
`ShapeSample`:
```rust
// Get a single random Vec3:
let sample: Vec3 = match *mode {
    Mode::Interior => shape.0.sample_interior(rng),
    Mode::Boundary => shape.0.sample_boundary(rng),
};
```

```rust
// Get 100 random Vec3s:
let samples: Vec<Vec3> = match *mode {
    Mode::Interior => {
        let dist = shape.0.interior_dist();
        dist.sample_iter(&mut rng).take(100).collect()
    }
    Mode::Boundary => {
        let dist = shape.0.boundary_dist();
        dist.sample_iter(&mut rng).take(100).collect()
    }
};
```

## Testing

Run the example!

## Discussion

Maybe in the future it would be nice to show off all of the different
shapes that we have implemented `ShapeSample` for, but I wanted to start
just by demonstrating the functionality. Here, I chose a cube because
it's simple and because it looks good rendered transparently with
backface culling disabled.
  • Loading branch information
mweatherley authored May 27, 2024
1 parent f398674 commit 787df44
Show file tree
Hide file tree
Showing 3 changed files with 268 additions and 0 deletions.
11 changes: 11 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2970,6 +2970,17 @@ description = "Shows off rendering for all math primitives as both Meshes and Gi
category = "Math"
wasm = true

[[example]]
name = "random_sampling"
path = "examples/math/random_sampling.rs"
doc-scrape-examples = true

[package.metadata.example.random_sampling]
name = "Random Sampling"
description = "Demonstrates how to sample random points from mathematical primitives"
category = "Math"
wasm = true

# Gizmos
[[example]]
name = "2d_gizmos"
Expand Down
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,7 @@ Example | Description

Example | Description
--- | ---
[Random Sampling](../examples/math/random_sampling.rs) | Demonstrates how to sample random points from mathematical primitives
[Rendering Primitives](../examples/math/render_primitives.rs) | Shows off rendering for all math primitives as both Meshes and Gizmos

## Reflection
Expand Down
256 changes: 256 additions & 0 deletions examples/math/random_sampling.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
//! This example shows how to sample random points from primitive shapes.
use bevy::{
input::mouse::{MouseButtonInput, MouseMotion},
math::prelude::*,
prelude::*,
render::mesh::SphereKind,
};
use rand::{distributions::Distribution, SeedableRng};
use rand_chacha::ChaCha8Rng;

fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Startup, setup)
.add_systems(Update, (handle_mouse, handle_keypress))
.run();
}

/// Resource for the random sampling mode, telling whether to sample the interior or the boundary.
#[derive(Resource)]
enum Mode {
Interior,
Boundary,
}

/// Resource storing the shape being sampled.
#[derive(Resource)]
struct SampledShape(Cuboid);

/// The source of randomness used by this example.
#[derive(Resource)]
struct RandomSource(ChaCha8Rng);

/// A container for the handle storing the mesh used to display sampled points as spheres.
#[derive(Resource)]
struct PointMesh(Handle<Mesh>);

/// A container for the handle storing the material used to display sampled points.
#[derive(Resource)]
struct PointMaterial(Handle<StandardMaterial>);

/// Marker component for sampled points.
#[derive(Component)]
struct SamplePoint;

/// The pressed state of the mouse, used for camera motion.
#[derive(Resource)]
struct MousePressed(bool);

fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
// Use seeded rng and store it in a resource; this makes the random output reproducible.
let seeded_rng = ChaCha8Rng::seed_from_u64(19878367467712);
commands.insert_resource(RandomSource(seeded_rng));

// Make a plane for establishing space.
commands.spawn(PbrBundle {
mesh: meshes.add(Plane3d::default().mesh().size(12.0, 12.0)),
material: materials.add(Color::srgb(0.3, 0.5, 0.3)),
transform: Transform::from_xyz(0.0, -2.5, 0.0),
..default()
});

// Store the shape we sample from in a resource:
let shape = Cuboid::from_length(2.9);
commands.insert_resource(SampledShape(shape));

// The sampled shape shown transparently:
commands.spawn(PbrBundle {
mesh: meshes.add(shape),
material: materials.add(StandardMaterial {
base_color: Color::srgba(0.2, 0.1, 0.6, 0.3),
alpha_mode: AlphaMode::Blend,
cull_mode: None,
..default()
}),
..default()
});

// A light:
commands.spawn(PointLightBundle {
point_light: PointLight {
shadows_enabled: true,
..default()
},
transform: Transform::from_xyz(4.0, 8.0, 4.0),
..default()
});

// A camera:
commands.spawn(Camera3dBundle {
transform: Transform::from_xyz(-2.0, 3.0, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
..default()
});

// Store the mesh and material for sample points in resources:
commands.insert_resource(PointMesh(
meshes.add(
Sphere::new(0.03)
.mesh()
.kind(SphereKind::Ico { subdivisions: 3 }),
),
));
commands.insert_resource(PointMaterial(materials.add(StandardMaterial {
base_color: Color::srgb(1.0, 0.8, 0.8),
metallic: 0.8,
..default()
})));

// Instructions for the example:
commands.spawn(
TextBundle::from_section(
"Controls:\n\
M: Toggle between sampling boundary and interior.\n\
R: Restart (erase all samples).\n\
S: Add one random sample.\n\
D: Add 100 random samples.\n\
Rotate camera by panning left/right.",
TextStyle {
font_size: 20.,
..default()
},
)
.with_style(Style {
position_type: PositionType::Absolute,
top: Val::Px(12.0),
left: Val::Px(12.0),
..default()
}),
);

// The mode starts with interior points.
commands.insert_resource(Mode::Interior);

// Starting mouse-pressed state is false.
commands.insert_resource(MousePressed(false));
}

// Handle user inputs from the keyboard:
#[allow(clippy::too_many_arguments)]
fn handle_keypress(
mut commands: Commands,
keyboard: Res<ButtonInput<KeyCode>>,
mut mode: ResMut<Mode>,
shape: Res<SampledShape>,
mut random_source: ResMut<RandomSource>,
sample_mesh: Res<PointMesh>,
sample_material: Res<PointMaterial>,
samples: Query<Entity, With<SamplePoint>>,
) {
// R => restart, deleting all samples
if keyboard.just_pressed(KeyCode::KeyR) {
for entity in &samples {
commands.entity(entity).despawn();
}
}

// S => sample once
if keyboard.just_pressed(KeyCode::KeyS) {
let rng = &mut random_source.0;

// Get a single random Vec3:
let sample: Vec3 = match *mode {
Mode::Interior => shape.0.sample_interior(rng),
Mode::Boundary => shape.0.sample_boundary(rng),
};

// Spawn a sphere at the random location:
commands.spawn((
PbrBundle {
mesh: sample_mesh.0.clone(),
material: sample_material.0.clone(),
transform: Transform::from_translation(sample),
..default()
},
SamplePoint,
));

// NOTE: The point is inside the cube created at setup just because of how the
// scene is constructed; in general, you would want to use something like
// `cube_transform.transform_point(sample)` to get the position of where the sample
// would be after adjusting for the position and orientation of the cube.
//
// If the spawned point also needed to follow the position of the cube as it moved,
// then making it a child entity of the cube would be a good approach.
}

// D => generate many samples
if keyboard.just_pressed(KeyCode::KeyD) {
let mut rng = &mut random_source.0;

// Get 100 random Vec3s:
let samples: Vec<Vec3> = match *mode {
Mode::Interior => {
let dist = shape.0.interior_dist();
dist.sample_iter(&mut rng).take(100).collect()
}
Mode::Boundary => {
let dist = shape.0.boundary_dist();
dist.sample_iter(&mut rng).take(100).collect()
}
};

// For each sample point, spawn a sphere:
for sample in samples {
commands.spawn((
PbrBundle {
mesh: sample_mesh.0.clone(),
material: sample_material.0.clone(),
transform: Transform::from_translation(sample),
..default()
},
SamplePoint,
));
}

// NOTE: See the previous note above regarding the positioning of these samples
// relative to the transform of the cube containing them.
}

// M => toggle mode between interior and boundary.
if keyboard.just_pressed(KeyCode::KeyM) {
match *mode {
Mode::Interior => *mode = Mode::Boundary,
Mode::Boundary => *mode = Mode::Interior,
}
}
}

// Handle user mouse input for panning the camera around:
fn handle_mouse(
mut button_events: EventReader<MouseButtonInput>,
mut motion_events: EventReader<MouseMotion>,
mut camera: Query<&mut Transform, With<Camera>>,
mut mouse_pressed: ResMut<MousePressed>,
) {
// Store left-pressed state in the MousePressed resource
for button_event in button_events.read() {
if button_event.button != MouseButton::Left {
continue;
}
*mouse_pressed = MousePressed(button_event.state.is_pressed());
}

// If the mouse is not pressed, just ignore motion events
if !mouse_pressed.0 {
return;
}
let displacement: f32 = motion_events.read().map(|motion| motion.delta.x).sum();
let mut camera_transform = camera.single_mut();
camera_transform.rotate_around(Vec3::ZERO, Quat::from_rotation_y(-displacement / 150.));
}

0 comments on commit 787df44

Please sign in to comment.