1use std::f32::consts::PI;
4
5use bevy::{
6 core_pipeline::tonemapping::Tonemapping,
7 input::mouse::{AccumulatedMouseMotion, AccumulatedMouseScroll, MouseButtonInput},
8 math::prelude::*,
9 post_process::bloom::Bloom,
10 prelude::*,
11};
12use rand::{seq::IndexedRandom, Rng, SeedableRng};
13use rand_chacha::ChaCha8Rng;
14
15fn 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
36const MAX_CAMERA_DISTANCE: f32 = 12.0;
41
42const MIN_CAMERA_DISTANCE: f32 = 1.0;
45
46const DISTANCE_BETWEEN_SHAPES: Vec3 = Vec3::new(2.0, 0.0, 0.0);
48
49const MAX_POINTS: usize = 3000; const POINTS_PER_FRAME: usize = 3;
55
56const INSIDE_POINT_COLOR: LinearRgba = LinearRgba::rgb(0.855, 1.1, 0.01);
58const BOUNDARY_POINT_COLOR: LinearRgba = LinearRgba::rgb(0.08, 0.2, 0.90);
60
61const ANIMATION_TIME: f32 = 1.0;
63
64const SKY_COLOR: Color = Color::srgb(0.02, 0.06, 0.15);
66
67const SMALL_3D: f32 = 0.5;
68const BIG_3D: f32 = 1.0;
69
70const CUBOID: Cuboid = Cuboid {
73 half_size: Vec3::new(SMALL_3D, BIG_3D, SMALL_3D),
74};
75
76const SPHERE: Sphere = Sphere {
77 radius: 1.5 * SMALL_3D,
78};
79
80const 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
88const CAPSULE_3D: Capsule3d = Capsule3d {
89 radius: SMALL_3D,
90 half_length: SMALL_3D,
91};
92
93const CYLINDER: Cylinder = Cylinder {
94 radius: SMALL_3D,
95 half_height: SMALL_3D,
96};
97
98const 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#[derive(Resource)]
111enum SamplingMode {
112 Interior,
113 Boundary,
114}
115
116#[derive(Resource)]
118enum SpawningMode {
119 Manual,
120 Automatic,
121}
122
123#[derive(Resource)]
125struct SpawnQueue(usize);
126
127#[derive(Resource)]
128struct PointCounter(usize);
129
130#[derive(Resource)]
132struct SampledShapes(Vec<(Shape, Vec3)>);
133
134impl 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#[derive(Clone, Copy)]
149enum Shape {
150 Cuboid,
151 Sphere,
152 Capsule,
153 Cylinder,
154 Tetrahedron,
155 Triangle,
156}
157struct ShapeMeshBuilder {
158 shape: Shape,
159}
160
161impl Shape {
162 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
175impl 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
200impl Meshable for Shape {
201 type Output = ShapeMeshBuilder;
202
203 fn mesh(&self) -> Self::Output {
204 ShapeMeshBuilder { shape: *self }
205 }
206}
207
208impl 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#[derive(Resource)]
223struct RandomSource(ChaCha8Rng);
224
225#[derive(Resource)]
227struct PointMesh(Handle<Mesh>);
228
229#[derive(Resource)]
231struct PointMaterial {
232 interior: Handle<StandardMaterial>,
233 boundary: Handle<StandardMaterial>,
234}
235
236#[derive(Component)]
238struct SamplePoint;
239
240#[derive(Component)]
242struct SpawningPoint {
243 progress: f32,
244}
245
246#[derive(Component)]
248struct DespawningPoint {
249 progress: f32,
250}
251
252#[derive(Component)]
254struct FireflyLights;
255
256#[derive(Resource)]
258struct MousePressed(bool);
259
260#[derive(Component)]
262struct CameraRig {
263 pub yaw: f32,
266 pub pitch: f32,
269 pub distance: f32,
271 pub target: Vec3,
273}
274
275fn setup(
276 mut commands: Commands,
277 mut meshes: ResMut<Assets<Mesh>>,
278 mut materials: ResMut<Assets<StandardMaterial>>,
279 shapes: Res<SampledShapes>,
280) {
281 let seeded_rng = ChaCha8Rng::seed_from_u64(4); commands.insert_resource(RandomSource(seeded_rng));
284
285 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 for (shape, translation) in shapes.0.iter() {
307 commands.spawn((
309 Mesh3d(meshes.add(shape.mesh())),
310 MeshMaterial3d(shape_material.clone()),
311 Transform::from_translation(*translation),
312 ));
313
314 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 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 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 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 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 commands.insert_resource(SpawnQueue(0));
401
402 commands.insert_resource(PointCounter(0));
404
405 commands.insert_resource(SamplingMode::Interior);
407
408 commands.insert_resource(SpawningMode::Automatic);
410
411 commands.insert_resource(MousePressed(false));
413}
414
415fn 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 if keyboard.just_pressed(KeyCode::KeyR) {
430 counter.0 = 0;
432 for entity in &samples {
433 commands.entity(entity).despawn();
434 }
435 }
436
437 if keyboard.just_pressed(KeyCode::KeyS) {
439 spawn_queue.0 += 1;
440 }
441
442 if keyboard.just_pressed(KeyCode::KeyD) {
444 spawn_queue.0 += 100;
445 }
446
447 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 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 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 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 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
511fn handle_mouse(
513 accumulated_mouse_motion: Res<AccumulatedMouseMotion>,
514 accumulated_mouse_scroll: Res<AccumulatedMouseScroll>,
515 mut mouse_button_inputs: MessageReader<MouseButtonInput>,
516 mut camera_rig: Single<&mut CameraRig>,
517 mut mouse_pressed: ResMut<MousePressed>,
518) {
519 for mouse_button_input in mouse_button_inputs.read() {
521 if mouse_button_input.button != MouseButton::Left {
522 continue;
523 }
524 *mouse_pressed = MousePressed(mouse_button_input.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 !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 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 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 let sample: Vec3 = *offset
581 + match *mode {
582 SamplingMode::Interior => shape.sample_interior(rng),
583 SamplingMode::Boundary => shape.sample_boundary(rng),
584 };
585
586 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 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 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
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 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}