From 787df44288d50a1acc02658420056902d7c25b26 Mon Sep 17 00:00:00 2001 From: Matty Date: Mon, 27 May 2024 09:46:11 -0400 Subject: [PATCH] Example for random sampling (#13507) # 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: Screenshot 2024-05-25 at 1 16 08 PM 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 = 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. --- Cargo.toml | 11 ++ examples/README.md | 1 + examples/math/random_sampling.rs | 256 +++++++++++++++++++++++++++++++ 3 files changed, 268 insertions(+) create mode 100644 examples/math/random_sampling.rs diff --git a/Cargo.toml b/Cargo.toml index 3e3ff2e72f..b3c30eef8d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/examples/README.md b/examples/README.md index 9326d3cdbb..89a88c9a5b 100644 --- a/examples/README.md +++ b/examples/README.md @@ -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 diff --git a/examples/math/random_sampling.rs b/examples/math/random_sampling.rs new file mode 100644 index 0000000000..f7ca2d4c10 --- /dev/null +++ b/examples/math/random_sampling.rs @@ -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); + +/// A container for the handle storing the material used to display sampled points. +#[derive(Resource)] +struct PointMaterial(Handle); + +/// 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>, + mut materials: ResMut>, +) { + // 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>, + mut mode: ResMut, + shape: Res, + mut random_source: ResMut, + sample_mesh: Res, + sample_material: Res, + samples: Query>, +) { + // 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 = 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, + mut motion_events: EventReader, + mut camera: Query<&mut Transform, With>, + mut mouse_pressed: ResMut, +) { + // 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.)); +}