random_sampling/
random_sampling.rs

1//! This example shows how to sample random points from primitive shapes.
2
3use bevy::{
4    input::mouse::{AccumulatedMouseMotion, MouseButtonInput},
5    math::prelude::*,
6    prelude::*,
7    render::mesh::SphereKind,
8};
9use rand::{distributions::Distribution, SeedableRng};
10use rand_chacha::ChaCha8Rng;
11
12fn main() {
13    App::new()
14        .add_plugins(DefaultPlugins)
15        .add_systems(Startup, setup)
16        .add_systems(Update, (handle_mouse, handle_keypress))
17        .run();
18}
19
20/// Resource for the random sampling mode, telling whether to sample the interior or the boundary.
21#[derive(Resource)]
22enum Mode {
23    Interior,
24    Boundary,
25}
26
27/// Resource storing the shape being sampled.
28#[derive(Resource)]
29struct SampledShape(Cuboid);
30
31/// The source of randomness used by this example.
32#[derive(Resource)]
33struct RandomSource(ChaCha8Rng);
34
35/// A container for the handle storing the mesh used to display sampled points as spheres.
36#[derive(Resource)]
37struct PointMesh(Handle<Mesh>);
38
39/// A container for the handle storing the material used to display sampled points.
40#[derive(Resource)]
41struct PointMaterial(Handle<StandardMaterial>);
42
43/// Marker component for sampled points.
44#[derive(Component)]
45struct SamplePoint;
46
47/// The pressed state of the mouse, used for camera motion.
48#[derive(Resource)]
49struct MousePressed(bool);
50
51fn setup(
52    mut commands: Commands,
53    mut meshes: ResMut<Assets<Mesh>>,
54    mut materials: ResMut<Assets<StandardMaterial>>,
55) {
56    // Use seeded rng and store it in a resource; this makes the random output reproducible.
57    let seeded_rng = ChaCha8Rng::seed_from_u64(19878367467712);
58    commands.insert_resource(RandomSource(seeded_rng));
59
60    // Make a plane for establishing space.
61    commands.spawn((
62        Mesh3d(meshes.add(Plane3d::default().mesh().size(12.0, 12.0))),
63        MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))),
64        Transform::from_xyz(0.0, -2.5, 0.0),
65    ));
66
67    // Store the shape we sample from in a resource:
68    let shape = Cuboid::from_length(2.9);
69    commands.insert_resource(SampledShape(shape));
70
71    // The sampled shape shown transparently:
72    commands.spawn((
73        Mesh3d(meshes.add(shape)),
74        MeshMaterial3d(materials.add(StandardMaterial {
75            base_color: Color::srgba(0.2, 0.1, 0.6, 0.3),
76            alpha_mode: AlphaMode::Blend,
77            cull_mode: None,
78            ..default()
79        })),
80    ));
81
82    // A light:
83    commands.spawn((
84        PointLight {
85            shadows_enabled: true,
86            ..default()
87        },
88        Transform::from_xyz(4.0, 8.0, 4.0),
89    ));
90
91    // A camera:
92    commands.spawn((
93        Camera3d::default(),
94        Transform::from_xyz(-2.0, 3.0, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
95    ));
96
97    // Store the mesh and material for sample points in resources:
98    commands.insert_resource(PointMesh(
99        meshes.add(
100            Sphere::new(0.03)
101                .mesh()
102                .kind(SphereKind::Ico { subdivisions: 3 }),
103        ),
104    ));
105    commands.insert_resource(PointMaterial(materials.add(StandardMaterial {
106        base_color: Color::srgb(1.0, 0.8, 0.8),
107        metallic: 0.8,
108        ..default()
109    })));
110
111    // Instructions for the example:
112    commands.spawn((
113        Text::new(
114            "Controls:\n\
115            M: Toggle between sampling boundary and interior.\n\
116            R: Restart (erase all samples).\n\
117            S: Add one random sample.\n\
118            D: Add 100 random samples.\n\
119            Rotate camera by holding left mouse and panning left/right.",
120        ),
121        Node {
122            position_type: PositionType::Absolute,
123            top: Val::Px(12.0),
124            left: Val::Px(12.0),
125            ..default()
126        },
127    ));
128
129    // The mode starts with interior points.
130    commands.insert_resource(Mode::Interior);
131
132    // Starting mouse-pressed state is false.
133    commands.insert_resource(MousePressed(false));
134}
135
136// Handle user inputs from the keyboard:
137fn handle_keypress(
138    mut commands: Commands,
139    keyboard: Res<ButtonInput<KeyCode>>,
140    mut mode: ResMut<Mode>,
141    shape: Res<SampledShape>,
142    mut random_source: ResMut<RandomSource>,
143    sample_mesh: Res<PointMesh>,
144    sample_material: Res<PointMaterial>,
145    samples: Query<Entity, With<SamplePoint>>,
146) {
147    // R => restart, deleting all samples
148    if keyboard.just_pressed(KeyCode::KeyR) {
149        for entity in &samples {
150            commands.entity(entity).despawn();
151        }
152    }
153
154    // S => sample once
155    if keyboard.just_pressed(KeyCode::KeyS) {
156        let rng = &mut random_source.0;
157
158        // Get a single random Vec3:
159        let sample: Vec3 = match *mode {
160            Mode::Interior => shape.0.sample_interior(rng),
161            Mode::Boundary => shape.0.sample_boundary(rng),
162        };
163
164        // Spawn a sphere at the random location:
165        commands.spawn((
166            Mesh3d(sample_mesh.0.clone()),
167            MeshMaterial3d(sample_material.0.clone()),
168            Transform::from_translation(sample),
169            SamplePoint,
170        ));
171
172        // NOTE: The point is inside the cube created at setup just because of how the
173        // scene is constructed; in general, you would want to use something like
174        // `cube_transform.transform_point(sample)` to get the position of where the sample
175        // would be after adjusting for the position and orientation of the cube.
176        //
177        // If the spawned point also needed to follow the position of the cube as it moved,
178        // then making it a child entity of the cube would be a good approach.
179    }
180
181    // D => generate many samples
182    if keyboard.just_pressed(KeyCode::KeyD) {
183        let mut rng = &mut random_source.0;
184
185        // Get 100 random Vec3s:
186        let samples: Vec<Vec3> = match *mode {
187            Mode::Interior => {
188                let dist = shape.0.interior_dist();
189                dist.sample_iter(&mut rng).take(100).collect()
190            }
191            Mode::Boundary => {
192                let dist = shape.0.boundary_dist();
193                dist.sample_iter(&mut rng).take(100).collect()
194            }
195        };
196
197        // For each sample point, spawn a sphere:
198        for sample in samples {
199            commands.spawn((
200                Mesh3d(sample_mesh.0.clone()),
201                MeshMaterial3d(sample_material.0.clone()),
202                Transform::from_translation(sample),
203                SamplePoint,
204            ));
205        }
206
207        // NOTE: See the previous note above regarding the positioning of these samples
208        // relative to the transform of the cube containing them.
209    }
210
211    // M => toggle mode between interior and boundary.
212    if keyboard.just_pressed(KeyCode::KeyM) {
213        match *mode {
214            Mode::Interior => *mode = Mode::Boundary,
215            Mode::Boundary => *mode = Mode::Interior,
216        }
217    }
218}
219
220// Handle user mouse input for panning the camera around:
221fn handle_mouse(
222    accumulated_mouse_motion: Res<AccumulatedMouseMotion>,
223    mut button_events: EventReader<MouseButtonInput>,
224    mut camera_transform: Single<&mut Transform, With<Camera>>,
225    mut mouse_pressed: ResMut<MousePressed>,
226) {
227    // Store left-pressed state in the MousePressed resource
228    for button_event in button_events.read() {
229        if button_event.button != MouseButton::Left {
230            continue;
231        }
232        *mouse_pressed = MousePressed(button_event.state.is_pressed());
233    }
234
235    // If the mouse is not pressed, just ignore motion events
236    if !mouse_pressed.0 {
237        return;
238    }
239    if accumulated_mouse_motion.delta != Vec2::ZERO {
240        let displacement = accumulated_mouse_motion.delta.x;
241        camera_transform.rotate_around(Vec3::ZERO, Quat::from_rotation_y(-displacement / 150.));
242    }
243}