sampling_primitives/
sampling_primitives.rs

1//! This example shows how to sample random points from primitive shapes.
2
3use std::f32::consts::PI;
4
5use bevy::{
6    core_pipeline::{bloom::Bloom, tonemapping::Tonemapping},
7    input::mouse::{AccumulatedMouseMotion, AccumulatedMouseScroll, MouseButtonInput},
8    math::prelude::*,
9    prelude::*,
10};
11use rand::{seq::SliceRandom, Rng, SeedableRng};
12use rand_chacha::ChaCha8Rng;
13
14fn main() {
15    App::new()
16        .add_plugins(DefaultPlugins)
17        .insert_resource(SampledShapes::new())
18        .add_systems(Startup, setup)
19        .add_systems(
20            Update,
21            (
22                handle_mouse,
23                handle_keypress,
24                spawn_points,
25                despawn_points,
26                animate_spawning,
27                animate_despawning,
28                update_camera,
29                update_lights,
30            ),
31        )
32        .run();
33}
34
35// Constants
36
37/// Maximum distance of the camera from its target. (meters)
38/// Should be set such that it is possible to look at all objects
39const MAX_CAMERA_DISTANCE: f32 = 12.0;
40
41/// Minimum distance of the camera from its target. (meters)
42/// Should be set such that it is not possible to clip into objects
43const MIN_CAMERA_DISTANCE: f32 = 1.0;
44
45/// Offset to be placed between the shapes
46const DISTANCE_BETWEEN_SHAPES: Vec3 = Vec3::new(2.0, 0.0, 0.0);
47
48/// Maximum amount of points allowed to be present.
49/// Should be set such that it does not cause large amounts of lag when reached.
50const MAX_POINTS: usize = 3000; // TODO: Test wasm and add a wasm-specific-bound
51
52/// How many points should be spawned each frame
53const POINTS_PER_FRAME: usize = 3;
54
55/// Color used for the inside points
56const INSIDE_POINT_COLOR: LinearRgba = LinearRgba::rgb(0.855, 1.1, 0.01);
57/// Color used for the points on the boundary
58const BOUNDARY_POINT_COLOR: LinearRgba = LinearRgba::rgb(0.08, 0.2, 0.90);
59
60/// Time (in seconds) for the spawning/despawning animation
61const ANIMATION_TIME: f32 = 1.0;
62
63/// Color for the sky and the sky-light
64const SKY_COLOR: Color = Color::srgb(0.02, 0.06, 0.15);
65
66const SMALL_3D: f32 = 0.5;
67const BIG_3D: f32 = 1.0;
68
69// primitives
70
71const CUBOID: Cuboid = Cuboid {
72    half_size: Vec3::new(SMALL_3D, BIG_3D, SMALL_3D),
73};
74
75const SPHERE: Sphere = Sphere {
76    radius: 1.5 * SMALL_3D,
77};
78
79const TRIANGLE_3D: Triangle3d = Triangle3d {
80    vertices: [
81        Vec3::new(BIG_3D, -BIG_3D * 0.5, 0.0),
82        Vec3::new(0.0, BIG_3D, 0.0),
83        Vec3::new(-BIG_3D, -BIG_3D * 0.5, 0.0),
84    ],
85};
86
87const CAPSULE_3D: Capsule3d = Capsule3d {
88    radius: SMALL_3D,
89    half_length: SMALL_3D,
90};
91
92const CYLINDER: Cylinder = Cylinder {
93    radius: SMALL_3D,
94    half_height: SMALL_3D,
95};
96
97const TETRAHEDRON: Tetrahedron = Tetrahedron {
98    vertices: [
99        Vec3::new(-BIG_3D, -BIG_3D * 0.67, BIG_3D * 0.5),
100        Vec3::new(BIG_3D, -BIG_3D * 0.67, BIG_3D * 0.5),
101        Vec3::new(0.0, -BIG_3D * 0.67, -BIG_3D * 1.17),
102        Vec3::new(0.0, BIG_3D, 0.0),
103    ],
104};
105
106// Components, Resources
107
108/// Resource for the random sampling mode, telling whether to sample the interior or the boundary.
109#[derive(Resource)]
110enum SamplingMode {
111    Interior,
112    Boundary,
113}
114
115/// Resource for storing whether points should spawn by themselves
116#[derive(Resource)]
117enum SpawningMode {
118    Manual,
119    Automatic,
120}
121
122/// Resource for tracking how many points should be spawned
123#[derive(Resource)]
124struct SpawnQueue(usize);
125
126#[derive(Resource)]
127struct PointCounter(usize);
128
129/// Resource storing the shapes being sampled and their translations.
130#[derive(Resource)]
131struct SampledShapes(Vec<(Shape, Vec3)>);
132
133impl SampledShapes {
134    fn new() -> Self {
135        let shapes = Shape::list_all_shapes();
136
137        let n_shapes = shapes.len();
138
139        let translations =
140            (0..n_shapes).map(|i| (i as f32 - n_shapes as f32 / 2.0) * DISTANCE_BETWEEN_SHAPES);
141
142        SampledShapes(shapes.into_iter().zip(translations).collect())
143    }
144}
145
146/// Enum listing the shapes that can be sampled
147#[derive(Clone, Copy)]
148enum Shape {
149    Cuboid,
150    Sphere,
151    Capsule,
152    Cylinder,
153    Tetrahedron,
154    Triangle,
155}
156struct ShapeMeshBuilder {
157    shape: Shape,
158}
159
160impl Shape {
161    /// Return a vector containing all implemented shapes
162    fn list_all_shapes() -> Vec<Shape> {
163        vec![
164            Shape::Cuboid,
165            Shape::Sphere,
166            Shape::Capsule,
167            Shape::Cylinder,
168            Shape::Tetrahedron,
169            Shape::Triangle,
170        ]
171    }
172}
173
174impl ShapeSample for Shape {
175    type Output = Vec3;
176    fn sample_interior<R: Rng + ?Sized>(&self, rng: &mut R) -> Vec3 {
177        match self {
178            Shape::Cuboid => CUBOID.sample_interior(rng),
179            Shape::Sphere => SPHERE.sample_interior(rng),
180            Shape::Capsule => CAPSULE_3D.sample_interior(rng),
181            Shape::Cylinder => CYLINDER.sample_interior(rng),
182            Shape::Tetrahedron => TETRAHEDRON.sample_interior(rng),
183            Shape::Triangle => TRIANGLE_3D.sample_interior(rng),
184        }
185    }
186
187    fn sample_boundary<R: Rng + ?Sized>(&self, rng: &mut R) -> Self::Output {
188        match self {
189            Shape::Cuboid => CUBOID.sample_boundary(rng),
190            Shape::Sphere => SPHERE.sample_boundary(rng),
191            Shape::Capsule => CAPSULE_3D.sample_boundary(rng),
192            Shape::Cylinder => CYLINDER.sample_boundary(rng),
193            Shape::Tetrahedron => TETRAHEDRON.sample_boundary(rng),
194            Shape::Triangle => TRIANGLE_3D.sample_boundary(rng),
195        }
196    }
197}
198
199impl Meshable for Shape {
200    type Output = ShapeMeshBuilder;
201
202    fn mesh(&self) -> Self::Output {
203        ShapeMeshBuilder { shape: *self }
204    }
205}
206
207impl MeshBuilder for ShapeMeshBuilder {
208    fn build(&self) -> Mesh {
209        match self.shape {
210            Shape::Cuboid => CUBOID.mesh().into(),
211            Shape::Sphere => SPHERE.mesh().into(),
212            Shape::Capsule => CAPSULE_3D.mesh().into(),
213            Shape::Cylinder => CYLINDER.mesh().into(),
214            Shape::Tetrahedron => TETRAHEDRON.mesh().into(),
215            Shape::Triangle => TRIANGLE_3D.mesh().into(),
216        }
217    }
218}
219
220/// The source of randomness used by this example.
221#[derive(Resource)]
222struct RandomSource(ChaCha8Rng);
223
224/// A container for the handle storing the mesh used to display sampled points as spheres.
225#[derive(Resource)]
226struct PointMesh(Handle<Mesh>);
227
228/// A container for the handle storing the material used to display sampled points.
229#[derive(Resource)]
230struct PointMaterial {
231    interior: Handle<StandardMaterial>,
232    boundary: Handle<StandardMaterial>,
233}
234
235/// Marker component for sampled points.
236#[derive(Component)]
237struct SamplePoint;
238
239/// Component for animating the spawn animation of lights.
240#[derive(Component)]
241struct SpawningPoint {
242    progress: f32,
243}
244
245/// Marker component for lights which should change intensity.
246#[derive(Component)]
247struct DespawningPoint {
248    progress: f32,
249}
250
251/// Marker component for lights which should change intensity.
252#[derive(Component)]
253struct FireflyLights;
254
255/// The pressed state of the mouse, used for camera motion.
256#[derive(Resource)]
257struct MousePressed(bool);
258
259/// Camera movement component.
260#[derive(Component)]
261struct CameraRig {
262    /// Rotation around the vertical axis of the camera (radians).
263    /// Positive changes makes the camera look more from the right.
264    pub yaw: f32,
265    /// Rotation around the horizontal axis of the camera (radians) (-pi/2; pi/2).
266    /// Positive looks down from above.
267    pub pitch: f32,
268    /// Distance from the center, smaller distance causes more zoom.
269    pub distance: f32,
270    /// Location in 3D space at which the camera is looking and around which it is orbiting.
271    pub target: Vec3,
272}
273
274fn setup(
275    mut commands: Commands,
276    mut meshes: ResMut<Assets<Mesh>>,
277    mut materials: ResMut<Assets<StandardMaterial>>,
278    shapes: Res<SampledShapes>,
279) {
280    // Use seeded rng and store it in a resource; this makes the random output reproducible.
281    let seeded_rng = ChaCha8Rng::seed_from_u64(4); // Chosen by a fair die roll, guaranteed to be random.
282    commands.insert_resource(RandomSource(seeded_rng));
283
284    // Make a plane for establishing space.
285    commands.spawn((
286        Mesh3d(meshes.add(Plane3d::default().mesh().size(20.0, 20.0))),
287        MeshMaterial3d(materials.add(StandardMaterial {
288            base_color: Color::srgb(0.3, 0.5, 0.3),
289            perceptual_roughness: 0.95,
290            metallic: 0.0,
291            ..default()
292        })),
293        Transform::from_xyz(0.0, -2.5, 0.0),
294    ));
295
296    let shape_material = materials.add(StandardMaterial {
297        base_color: Color::srgba(0.2, 0.1, 0.6, 0.3),
298        reflectance: 0.0,
299        alpha_mode: AlphaMode::Blend,
300        cull_mode: None,
301        ..default()
302    });
303
304    // Spawn shapes to be sampled
305    for (shape, translation) in shapes.0.iter() {
306        // The sampled shape shown transparently:
307        commands.spawn((
308            Mesh3d(meshes.add(shape.mesh())),
309            MeshMaterial3d(shape_material.clone()),
310            Transform::from_translation(*translation),
311        ));
312
313        // Lights which work as the bulk lighting of the fireflies:
314        commands.spawn((
315            PointLight {
316                range: 4.0,
317                radius: 0.6,
318                intensity: 1.0,
319                shadows_enabled: false,
320                color: Color::LinearRgba(INSIDE_POINT_COLOR),
321                ..default()
322            },
323            Transform::from_translation(*translation),
324            FireflyLights,
325        ));
326    }
327
328    // Global light:
329    commands.spawn((
330        PointLight {
331            color: SKY_COLOR,
332            intensity: 2_000.0,
333            shadows_enabled: false,
334            ..default()
335        },
336        Transform::from_xyz(4.0, 8.0, 4.0),
337    ));
338
339    // A camera:
340    commands.spawn((
341        Camera3d::default(),
342        Camera {
343            hdr: true, // HDR is required for bloom
344            clear_color: ClearColorConfig::Custom(SKY_COLOR),
345            ..default()
346        },
347        Tonemapping::TonyMcMapface,
348        Transform::from_xyz(-2.0, 3.0, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
349        Bloom::NATURAL,
350        CameraRig {
351            yaw: 0.56,
352            pitch: 0.45,
353            distance: 8.0,
354            target: Vec3::ZERO,
355        },
356    ));
357
358    // Store the mesh and material for sample points in resources:
359    commands.insert_resource(PointMesh(
360        meshes.add(Sphere::new(0.03).mesh().ico(1).unwrap()),
361    ));
362    commands.insert_resource(PointMaterial {
363        interior: materials.add(StandardMaterial {
364            base_color: Color::BLACK,
365            reflectance: 0.05,
366            emissive: 2.5 * INSIDE_POINT_COLOR,
367            ..default()
368        }),
369        boundary: materials.add(StandardMaterial {
370            base_color: Color::BLACK,
371            reflectance: 0.05,
372            emissive: 1.5 * BOUNDARY_POINT_COLOR,
373            ..default()
374        }),
375    });
376
377    // Instructions for the example:
378    commands.spawn((
379        Text::new(
380            "Controls:\n\
381            M: Toggle between sampling boundary and interior.\n\
382            A: Toggle automatic spawning & despawning of points.\n\
383            R: Restart (erase all samples).\n\
384            S: Add one random sample.\n\
385            D: Add 100 random samples.\n\
386            Rotate camera by holding left mouse and panning.\n\
387            Zoom camera by scrolling via mouse or +/-.\n\
388            Move camera by L/R arrow keys.\n\
389            Tab: Toggle this text",
390        ),
391        Node {
392            position_type: PositionType::Absolute,
393            top: Val::Px(12.0),
394            left: Val::Px(12.0),
395            ..default()
396        },
397    ));
398
399    // No points are scheduled to spawn initially.
400    commands.insert_resource(SpawnQueue(0));
401
402    // No points have been spawned initially.
403    commands.insert_resource(PointCounter(0));
404
405    // The mode starts with interior points.
406    commands.insert_resource(SamplingMode::Interior);
407
408    // Points spawn automatically by default.
409    commands.insert_resource(SpawningMode::Automatic);
410
411    // Starting mouse-pressed state is false.
412    commands.insert_resource(MousePressed(false));
413}
414
415// Handle user inputs from the keyboard:
416fn handle_keypress(
417    mut commands: Commands,
418    keyboard: Res<ButtonInput<KeyCode>>,
419    mut mode: ResMut<SamplingMode>,
420    mut spawn_mode: ResMut<SpawningMode>,
421    samples: Query<Entity, With<SamplePoint>>,
422    shapes: Res<SampledShapes>,
423    mut spawn_queue: ResMut<SpawnQueue>,
424    mut counter: ResMut<PointCounter>,
425    mut text_menus: Query<&mut Visibility, With<Text>>,
426    mut camera_rig: Single<&mut CameraRig>,
427) {
428    // R => restart, deleting all samples
429    if keyboard.just_pressed(KeyCode::KeyR) {
430        // Don't forget to zero out the counter!
431        counter.0 = 0;
432        for entity in &samples {
433            commands.entity(entity).despawn();
434        }
435    }
436
437    // S => sample once
438    if keyboard.just_pressed(KeyCode::KeyS) {
439        spawn_queue.0 += 1;
440    }
441
442    // D => sample a hundred
443    if keyboard.just_pressed(KeyCode::KeyD) {
444        spawn_queue.0 += 100;
445    }
446
447    // M => toggle mode between interior and boundary.
448    if keyboard.just_pressed(KeyCode::KeyM) {
449        match *mode {
450            SamplingMode::Interior => *mode = SamplingMode::Boundary,
451            SamplingMode::Boundary => *mode = SamplingMode::Interior,
452        }
453    }
454
455    // A => toggle spawning mode between automatic and manual.
456    if keyboard.just_pressed(KeyCode::KeyA) {
457        match *spawn_mode {
458            SpawningMode::Manual => *spawn_mode = SpawningMode::Automatic,
459            SpawningMode::Automatic => *spawn_mode = SpawningMode::Manual,
460        }
461    }
462
463    // Tab => toggle help menu.
464    if keyboard.just_pressed(KeyCode::Tab) {
465        for mut visibility in text_menus.iter_mut() {
466            *visibility = match *visibility {
467                Visibility::Hidden => Visibility::Visible,
468                _ => Visibility::Hidden,
469            };
470        }
471    }
472
473    // +/- => zoom camera.
474    if keyboard.just_pressed(KeyCode::NumpadSubtract) || keyboard.just_pressed(KeyCode::Minus) {
475        camera_rig.distance += MAX_CAMERA_DISTANCE / 15.0;
476        camera_rig.distance = camera_rig
477            .distance
478            .clamp(MIN_CAMERA_DISTANCE, MAX_CAMERA_DISTANCE);
479    }
480
481    if keyboard.just_pressed(KeyCode::NumpadAdd) {
482        camera_rig.distance -= MAX_CAMERA_DISTANCE / 15.0;
483        camera_rig.distance = camera_rig
484            .distance
485            .clamp(MIN_CAMERA_DISTANCE, MAX_CAMERA_DISTANCE);
486    }
487
488    // Arrows => Move camera focus
489    let left = keyboard.just_pressed(KeyCode::ArrowLeft);
490    let right = keyboard.just_pressed(KeyCode::ArrowRight);
491
492    if left || right {
493        let mut closest = 0;
494        let mut closest_distance = f32::MAX;
495        for (i, (_, position)) in shapes.0.iter().enumerate() {
496            let distance = camera_rig.target.distance(*position);
497            if distance < closest_distance {
498                closest = i;
499                closest_distance = distance;
500            }
501        }
502        if closest > 0 && left {
503            camera_rig.target = shapes.0[closest - 1].1;
504        }
505        if closest < shapes.0.len() - 1 && right {
506            camera_rig.target = shapes.0[closest + 1].1;
507        }
508    }
509}
510
511// Handle user mouse input for panning the camera around:
512fn handle_mouse(
513    accumulated_mouse_motion: Res<AccumulatedMouseMotion>,
514    accumulated_mouse_scroll: Res<AccumulatedMouseScroll>,
515    mut button_events: EventReader<MouseButtonInput>,
516    mut camera_rig: Single<&mut CameraRig>,
517    mut mouse_pressed: ResMut<MousePressed>,
518) {
519    // Store left-pressed state in the MousePressed resource
520    for button_event in button_events.read() {
521        if button_event.button != MouseButton::Left {
522            continue;
523        }
524        *mouse_pressed = MousePressed(button_event.state.is_pressed());
525    }
526
527    if accumulated_mouse_scroll.delta != Vec2::ZERO {
528        let mouse_scroll = accumulated_mouse_scroll.delta.y;
529        camera_rig.distance -= mouse_scroll / 15.0 * MAX_CAMERA_DISTANCE;
530        camera_rig.distance = camera_rig
531            .distance
532            .clamp(MIN_CAMERA_DISTANCE, MAX_CAMERA_DISTANCE);
533    }
534
535    // If the mouse is not pressed, just ignore motion events
536    if !mouse_pressed.0 {
537        return;
538    }
539    if accumulated_mouse_motion.delta != Vec2::ZERO {
540        let displacement = accumulated_mouse_motion.delta;
541        camera_rig.yaw += displacement.x / 90.;
542        camera_rig.pitch += displacement.y / 90.;
543        // The extra 0.01 is to disallow weird behavior at the poles of the rotation
544        camera_rig.pitch = camera_rig.pitch.clamp(-PI / 2.01, PI / 2.01);
545    }
546}
547
548fn spawn_points(
549    mut commands: Commands,
550    mode: ResMut<SamplingMode>,
551    shapes: Res<SampledShapes>,
552    mut random_source: ResMut<RandomSource>,
553    sample_mesh: Res<PointMesh>,
554    sample_material: Res<PointMaterial>,
555    mut spawn_queue: ResMut<SpawnQueue>,
556    mut counter: ResMut<PointCounter>,
557    spawn_mode: ResMut<SpawningMode>,
558) {
559    if let SpawningMode::Automatic = *spawn_mode {
560        spawn_queue.0 += POINTS_PER_FRAME;
561    }
562
563    if spawn_queue.0 == 0 {
564        return;
565    }
566
567    let rng = &mut random_source.0;
568
569    // Don't go crazy
570    for _ in 0..1000 {
571        if spawn_queue.0 == 0 {
572            break;
573        }
574        spawn_queue.0 -= 1;
575        counter.0 += 1;
576
577        let (shape, offset) = shapes.0.choose(rng).expect("There is at least one shape");
578
579        // Get a single random Vec3:
580        let sample: Vec3 = *offset
581            + match *mode {
582                SamplingMode::Interior => shape.sample_interior(rng),
583                SamplingMode::Boundary => shape.sample_boundary(rng),
584            };
585
586        // Spawn a sphere at the random location:
587        commands.spawn((
588            Mesh3d(sample_mesh.0.clone()),
589            MeshMaterial3d(match *mode {
590                SamplingMode::Interior => sample_material.interior.clone(),
591                SamplingMode::Boundary => sample_material.boundary.clone(),
592            }),
593            Transform::from_translation(sample).with_scale(Vec3::ZERO),
594            SamplePoint,
595            SpawningPoint { progress: 0.0 },
596        ));
597    }
598}
599
600fn despawn_points(
601    mut commands: Commands,
602    samples: Query<Entity, With<SamplePoint>>,
603    spawn_mode: Res<SpawningMode>,
604    mut counter: ResMut<PointCounter>,
605    mut random_source: ResMut<RandomSource>,
606) {
607    // Do not despawn automatically in manual mode
608    if let SpawningMode::Manual = *spawn_mode {
609        return;
610    }
611
612    if counter.0 < MAX_POINTS {
613        return;
614    }
615
616    let rng = &mut random_source.0;
617    // Skip a random amount of points to ensure random despawning
618    let skip = rng.gen_range(0..counter.0);
619    let despawn_amount = (counter.0 - MAX_POINTS).min(100);
620    counter.0 -= samples
621        .iter()
622        .skip(skip)
623        .take(despawn_amount)
624        .map(|entity| {
625            commands
626                .entity(entity)
627                .insert(DespawningPoint { progress: 0.0 })
628                .remove::<SpawningPoint>()
629                .remove::<SamplePoint>();
630        })
631        .count();
632}
633
634fn animate_spawning(
635    mut commands: Commands,
636    time: Res<Time>,
637    mut samples: Query<(Entity, &mut Transform, &mut SpawningPoint)>,
638) {
639    let dt = time.delta_secs();
640
641    for (entity, mut transform, mut point) in samples.iter_mut() {
642        point.progress += dt / ANIMATION_TIME;
643        transform.scale = Vec3::splat(point.progress.min(1.0));
644        if point.progress >= 1.0 {
645            commands.entity(entity).remove::<SpawningPoint>();
646        }
647    }
648}
649
650fn animate_despawning(
651    mut commands: Commands,
652    time: Res<Time>,
653    mut samples: Query<(Entity, &mut Transform, &mut DespawningPoint)>,
654) {
655    let dt = time.delta_secs();
656
657    for (entity, mut transform, mut point) in samples.iter_mut() {
658        point.progress += dt / ANIMATION_TIME;
659        // If the point is already smaller than expected, jump ahead with the despawning progress to avoid sudden jumps in size
660        point.progress = f32::max(point.progress, 1.0 - transform.scale.x);
661        transform.scale = Vec3::splat((1.0 - point.progress).max(0.0));
662        if point.progress >= 1.0 {
663            commands.entity(entity).despawn();
664        }
665    }
666}
667
668fn update_camera(mut camera: Query<(&mut Transform, &CameraRig), Changed<CameraRig>>) {
669    for (mut transform, rig) in camera.iter_mut() {
670        let looking_direction =
671            Quat::from_rotation_y(-rig.yaw) * Quat::from_rotation_x(rig.pitch) * Vec3::Z;
672        transform.translation = rig.target - rig.distance * looking_direction;
673        transform.look_at(rig.target, Dir3::Y);
674    }
675}
676
677fn update_lights(
678    mut lights: Query<&mut PointLight, With<FireflyLights>>,
679    counter: Res<PointCounter>,
680) {
681    let saturation = (counter.0 as f32 / MAX_POINTS as f32).min(2.0);
682    let intensity = 40_000.0 * saturation;
683    for mut light in lights.iter_mut() {
684        light.intensity = light.intensity.lerp(intensity, 0.04);
685    }
686}