Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/examples/math/sampling_primitives.rs
6592 views
1
//! This example shows how to sample random points from primitive shapes.
2
3
use std::f32::consts::PI;
4
5
use bevy::{
6
core_pipeline::tonemapping::Tonemapping,
7
input::mouse::{AccumulatedMouseMotion, AccumulatedMouseScroll, MouseButtonInput},
8
math::prelude::*,
9
post_process::bloom::Bloom,
10
prelude::*,
11
};
12
use rand::{seq::IndexedRandom, Rng, SeedableRng};
13
use rand_chacha::ChaCha8Rng;
14
15
fn main() {
16
App::new()
17
.add_plugins(DefaultPlugins)
18
.insert_resource(SampledShapes::new())
19
.add_systems(Startup, setup)
20
.add_systems(
21
Update,
22
(
23
handle_mouse,
24
handle_keypress,
25
spawn_points,
26
despawn_points,
27
animate_spawning,
28
animate_despawning,
29
update_camera,
30
update_lights,
31
),
32
)
33
.run();
34
}
35
36
// Constants
37
38
/// Maximum distance of the camera from its target. (meters)
39
/// Should be set such that it is possible to look at all objects
40
const MAX_CAMERA_DISTANCE: f32 = 12.0;
41
42
/// Minimum distance of the camera from its target. (meters)
43
/// Should be set such that it is not possible to clip into objects
44
const MIN_CAMERA_DISTANCE: f32 = 1.0;
45
46
/// Offset to be placed between the shapes
47
const DISTANCE_BETWEEN_SHAPES: Vec3 = Vec3::new(2.0, 0.0, 0.0);
48
49
/// Maximum amount of points allowed to be present.
50
/// Should be set such that it does not cause large amounts of lag when reached.
51
const MAX_POINTS: usize = 3000; // TODO: Test wasm and add a wasm-specific-bound
52
53
/// How many points should be spawned each frame
54
const POINTS_PER_FRAME: usize = 3;
55
56
/// Color used for the inside points
57
const INSIDE_POINT_COLOR: LinearRgba = LinearRgba::rgb(0.855, 1.1, 0.01);
58
/// Color used for the points on the boundary
59
const BOUNDARY_POINT_COLOR: LinearRgba = LinearRgba::rgb(0.08, 0.2, 0.90);
60
61
/// Time (in seconds) for the spawning/despawning animation
62
const ANIMATION_TIME: f32 = 1.0;
63
64
/// Color for the sky and the sky-light
65
const SKY_COLOR: Color = Color::srgb(0.02, 0.06, 0.15);
66
67
const SMALL_3D: f32 = 0.5;
68
const BIG_3D: f32 = 1.0;
69
70
// primitives
71
72
const CUBOID: Cuboid = Cuboid {
73
half_size: Vec3::new(SMALL_3D, BIG_3D, SMALL_3D),
74
};
75
76
const SPHERE: Sphere = Sphere {
77
radius: 1.5 * SMALL_3D,
78
};
79
80
const TRIANGLE_3D: Triangle3d = Triangle3d {
81
vertices: [
82
Vec3::new(BIG_3D, -BIG_3D * 0.5, 0.0),
83
Vec3::new(0.0, BIG_3D, 0.0),
84
Vec3::new(-BIG_3D, -BIG_3D * 0.5, 0.0),
85
],
86
};
87
88
const CAPSULE_3D: Capsule3d = Capsule3d {
89
radius: SMALL_3D,
90
half_length: SMALL_3D,
91
};
92
93
const CYLINDER: Cylinder = Cylinder {
94
radius: SMALL_3D,
95
half_height: SMALL_3D,
96
};
97
98
const TETRAHEDRON: Tetrahedron = Tetrahedron {
99
vertices: [
100
Vec3::new(-BIG_3D, -BIG_3D * 0.67, BIG_3D * 0.5),
101
Vec3::new(BIG_3D, -BIG_3D * 0.67, BIG_3D * 0.5),
102
Vec3::new(0.0, -BIG_3D * 0.67, -BIG_3D * 1.17),
103
Vec3::new(0.0, BIG_3D, 0.0),
104
],
105
};
106
107
// Components, Resources
108
109
/// Resource for the random sampling mode, telling whether to sample the interior or the boundary.
110
#[derive(Resource)]
111
enum SamplingMode {
112
Interior,
113
Boundary,
114
}
115
116
/// Resource for storing whether points should spawn by themselves
117
#[derive(Resource)]
118
enum SpawningMode {
119
Manual,
120
Automatic,
121
}
122
123
/// Resource for tracking how many points should be spawned
124
#[derive(Resource)]
125
struct SpawnQueue(usize);
126
127
#[derive(Resource)]
128
struct PointCounter(usize);
129
130
/// Resource storing the shapes being sampled and their translations.
131
#[derive(Resource)]
132
struct SampledShapes(Vec<(Shape, Vec3)>);
133
134
impl SampledShapes {
135
fn new() -> Self {
136
let shapes = Shape::list_all_shapes();
137
138
let n_shapes = shapes.len();
139
140
let translations =
141
(0..n_shapes).map(|i| (i as f32 - n_shapes as f32 / 2.0) * DISTANCE_BETWEEN_SHAPES);
142
143
SampledShapes(shapes.into_iter().zip(translations).collect())
144
}
145
}
146
147
/// Enum listing the shapes that can be sampled
148
#[derive(Clone, Copy)]
149
enum Shape {
150
Cuboid,
151
Sphere,
152
Capsule,
153
Cylinder,
154
Tetrahedron,
155
Triangle,
156
}
157
struct ShapeMeshBuilder {
158
shape: Shape,
159
}
160
161
impl Shape {
162
/// Return a vector containing all implemented shapes
163
fn list_all_shapes() -> Vec<Shape> {
164
vec![
165
Shape::Cuboid,
166
Shape::Sphere,
167
Shape::Capsule,
168
Shape::Cylinder,
169
Shape::Tetrahedron,
170
Shape::Triangle,
171
]
172
}
173
}
174
175
impl ShapeSample for Shape {
176
type Output = Vec3;
177
fn sample_interior<R: Rng + ?Sized>(&self, rng: &mut R) -> Vec3 {
178
match self {
179
Shape::Cuboid => CUBOID.sample_interior(rng),
180
Shape::Sphere => SPHERE.sample_interior(rng),
181
Shape::Capsule => CAPSULE_3D.sample_interior(rng),
182
Shape::Cylinder => CYLINDER.sample_interior(rng),
183
Shape::Tetrahedron => TETRAHEDRON.sample_interior(rng),
184
Shape::Triangle => TRIANGLE_3D.sample_interior(rng),
185
}
186
}
187
188
fn sample_boundary<R: Rng + ?Sized>(&self, rng: &mut R) -> Self::Output {
189
match self {
190
Shape::Cuboid => CUBOID.sample_boundary(rng),
191
Shape::Sphere => SPHERE.sample_boundary(rng),
192
Shape::Capsule => CAPSULE_3D.sample_boundary(rng),
193
Shape::Cylinder => CYLINDER.sample_boundary(rng),
194
Shape::Tetrahedron => TETRAHEDRON.sample_boundary(rng),
195
Shape::Triangle => TRIANGLE_3D.sample_boundary(rng),
196
}
197
}
198
}
199
200
impl Meshable for Shape {
201
type Output = ShapeMeshBuilder;
202
203
fn mesh(&self) -> Self::Output {
204
ShapeMeshBuilder { shape: *self }
205
}
206
}
207
208
impl MeshBuilder for ShapeMeshBuilder {
209
fn build(&self) -> Mesh {
210
match self.shape {
211
Shape::Cuboid => CUBOID.mesh().into(),
212
Shape::Sphere => SPHERE.mesh().into(),
213
Shape::Capsule => CAPSULE_3D.mesh().into(),
214
Shape::Cylinder => CYLINDER.mesh().into(),
215
Shape::Tetrahedron => TETRAHEDRON.mesh().into(),
216
Shape::Triangle => TRIANGLE_3D.mesh().into(),
217
}
218
}
219
}
220
221
/// The source of randomness used by this example.
222
#[derive(Resource)]
223
struct RandomSource(ChaCha8Rng);
224
225
/// A container for the handle storing the mesh used to display sampled points as spheres.
226
#[derive(Resource)]
227
struct PointMesh(Handle<Mesh>);
228
229
/// A container for the handle storing the material used to display sampled points.
230
#[derive(Resource)]
231
struct PointMaterial {
232
interior: Handle<StandardMaterial>,
233
boundary: Handle<StandardMaterial>,
234
}
235
236
/// Marker component for sampled points.
237
#[derive(Component)]
238
struct SamplePoint;
239
240
/// Component for animating the spawn animation of lights.
241
#[derive(Component)]
242
struct SpawningPoint {
243
progress: f32,
244
}
245
246
/// Marker component for lights which should change intensity.
247
#[derive(Component)]
248
struct DespawningPoint {
249
progress: f32,
250
}
251
252
/// Marker component for lights which should change intensity.
253
#[derive(Component)]
254
struct FireflyLights;
255
256
/// The pressed state of the mouse, used for camera motion.
257
#[derive(Resource)]
258
struct MousePressed(bool);
259
260
/// Camera movement component.
261
#[derive(Component)]
262
struct CameraRig {
263
/// Rotation around the vertical axis of the camera (radians).
264
/// Positive changes makes the camera look more from the right.
265
pub yaw: f32,
266
/// Rotation around the horizontal axis of the camera (radians) (-pi/2; pi/2).
267
/// Positive looks down from above.
268
pub pitch: f32,
269
/// Distance from the center, smaller distance causes more zoom.
270
pub distance: f32,
271
/// Location in 3D space at which the camera is looking and around which it is orbiting.
272
pub target: Vec3,
273
}
274
275
fn setup(
276
mut commands: Commands,
277
mut meshes: ResMut<Assets<Mesh>>,
278
mut materials: ResMut<Assets<StandardMaterial>>,
279
shapes: Res<SampledShapes>,
280
) {
281
// Use seeded rng and store it in a resource; this makes the random output reproducible.
282
let seeded_rng = ChaCha8Rng::seed_from_u64(4); // Chosen by a fair die roll, guaranteed to be random.
283
commands.insert_resource(RandomSource(seeded_rng));
284
285
// Make a plane for establishing space.
286
commands.spawn((
287
Mesh3d(meshes.add(Plane3d::default().mesh().size(20.0, 20.0))),
288
MeshMaterial3d(materials.add(StandardMaterial {
289
base_color: Color::srgb(0.3, 0.5, 0.3),
290
perceptual_roughness: 0.95,
291
metallic: 0.0,
292
..default()
293
})),
294
Transform::from_xyz(0.0, -2.5, 0.0),
295
));
296
297
let shape_material = materials.add(StandardMaterial {
298
base_color: Color::srgba(0.2, 0.1, 0.6, 0.3),
299
reflectance: 0.0,
300
alpha_mode: AlphaMode::Blend,
301
cull_mode: None,
302
..default()
303
});
304
305
// Spawn shapes to be sampled
306
for (shape, translation) in shapes.0.iter() {
307
// The sampled shape shown transparently:
308
commands.spawn((
309
Mesh3d(meshes.add(shape.mesh())),
310
MeshMaterial3d(shape_material.clone()),
311
Transform::from_translation(*translation),
312
));
313
314
// Lights which work as the bulk lighting of the fireflies:
315
commands.spawn((
316
PointLight {
317
range: 4.0,
318
radius: 0.6,
319
intensity: 1.0,
320
shadows_enabled: false,
321
color: Color::LinearRgba(INSIDE_POINT_COLOR),
322
..default()
323
},
324
Transform::from_translation(*translation),
325
FireflyLights,
326
));
327
}
328
329
// Global light:
330
commands.spawn((
331
PointLight {
332
color: SKY_COLOR,
333
intensity: 2_000.0,
334
shadows_enabled: false,
335
..default()
336
},
337
Transform::from_xyz(4.0, 8.0, 4.0),
338
));
339
340
// A camera:
341
commands.spawn((
342
Camera3d::default(),
343
Camera {
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: px(12),
394
left: px(12),
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:
416
fn 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:
512
fn 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
548
fn 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
600
fn 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.random_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
634
fn 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
650
fn 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
668
fn 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
677
fn 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
}
687
688