//! This example shows how to sample random points from primitive shapes. use std::f32::consts::PI; use bevy::{ core_pipeline::{bloom::Bloom, tonemapping::Tonemapping}, input::mouse::{AccumulatedMouseMotion, AccumulatedMouseScroll, MouseButtonInput}, math::prelude::*, prelude::*, }; use rand::{seq::SliceRandom, Rng, SeedableRng}; use rand_chacha::ChaCha8Rng; fn main() { App::new() .add_plugins(DefaultPlugins) .insert_resource(SampledShapes::new()) .add_systems(Startup, setup) .add_systems( Update, ( handle_mouse, handle_keypress, spawn_points, despawn_points, animate_spawning, animate_despawning, update_camera, update_lights, ), ) .run(); } // Constants /// Maximum distance of the camera from its target. (meters) /// Should be set such that it is possible to look at all objects const MAX_CAMERA_DISTANCE: f32 = 12.0; /// Minimum distance of the camera from its target. (meters) /// Should be set such that it is not possible to clip into objects const MIN_CAMERA_DISTANCE: f32 = 1.0; /// Offset to be placed between the shapes const DISTANCE_BETWEEN_SHAPES: Vec3 = Vec3::new(2.0, 0.0, 0.0); /// Maximum amount of points allowed to be present. /// Should be set such that it does not cause large amounts of lag when reached. const MAX_POINTS: usize = 3000; // TODO: Test wasm and add a wasm-specific-bound /// How many points should be spawned each frame const POINTS_PER_FRAME: usize = 3; /// Color used for the inside points const INSIDE_POINT_COLOR: LinearRgba = LinearRgba::rgb(0.855, 1.1, 0.01); /// Color used for the points on the boundary const BOUNDARY_POINT_COLOR: LinearRgba = LinearRgba::rgb(0.08, 0.2, 0.90); /// Time (in seconds) for the spawning/despawning animation const ANIMATION_TIME: f32 = 1.0; /// Color for the sky and the sky-light const SKY_COLOR: Color = Color::srgb(0.02, 0.06, 0.15); const SMALL_3D: f32 = 0.5; const BIG_3D: f32 = 1.0; // primitives const CUBOID: Cuboid = Cuboid { half_size: Vec3::new(SMALL_3D, BIG_3D, SMALL_3D), }; const SPHERE: Sphere = Sphere { radius: 1.5 * SMALL_3D, }; const TRIANGLE_3D: Triangle3d = Triangle3d { vertices: [ Vec3::new(BIG_3D, -BIG_3D * 0.5, 0.0), Vec3::new(0.0, BIG_3D, 0.0), Vec3::new(-BIG_3D, -BIG_3D * 0.5, 0.0), ], }; const CAPSULE_3D: Capsule3d = Capsule3d { radius: SMALL_3D, half_length: SMALL_3D, }; const CYLINDER: Cylinder = Cylinder { radius: SMALL_3D, half_height: SMALL_3D, }; const TETRAHEDRON: Tetrahedron = Tetrahedron { vertices: [ Vec3::new(-BIG_3D, -BIG_3D * 0.67, BIG_3D * 0.5), Vec3::new(BIG_3D, -BIG_3D * 0.67, BIG_3D * 0.5), Vec3::new(0.0, -BIG_3D * 0.67, -BIG_3D * 1.17), Vec3::new(0.0, BIG_3D, 0.0), ], }; // Components, Resources /// Resource for the random sampling mode, telling whether to sample the interior or the boundary. #[derive(Resource)] enum SamplingMode { Interior, Boundary, } /// Resource for storing whether points should spawn by themselves #[derive(Resource)] enum SpawningMode { Manual, Automatic, } /// Resource for tracking how many points should be spawned #[derive(Resource)] struct SpawnQueue(usize); #[derive(Resource)] struct PointCounter(usize); /// Resource storing the shapes being sampled and their translations. #[derive(Resource)] struct SampledShapes(Vec<(Shape, Vec3)>); impl SampledShapes { fn new() -> Self { let shapes = Shape::list_all_shapes(); let n_shapes = shapes.len(); let translations = (0..n_shapes).map(|i| (i as f32 - n_shapes as f32 / 2.0) * DISTANCE_BETWEEN_SHAPES); SampledShapes(shapes.into_iter().zip(translations).collect()) } } /// Enum listing the shapes that can be sampled #[derive(Clone, Copy)] enum Shape { Cuboid, Sphere, Capsule, Cylinder, Tetrahedron, Triangle, } struct ShapeMeshBuilder { shape: Shape, } impl Shape { /// Return a vector containing all implemented shapes fn list_all_shapes() -> Vec { vec![ Shape::Cuboid, Shape::Sphere, Shape::Capsule, Shape::Cylinder, Shape::Tetrahedron, Shape::Triangle, ] } } impl ShapeSample for Shape { type Output = Vec3; fn sample_interior(&self, rng: &mut R) -> Vec3 { match self { Shape::Cuboid => CUBOID.sample_interior(rng), Shape::Sphere => SPHERE.sample_interior(rng), Shape::Capsule => CAPSULE_3D.sample_interior(rng), Shape::Cylinder => CYLINDER.sample_interior(rng), Shape::Tetrahedron => TETRAHEDRON.sample_interior(rng), Shape::Triangle => TRIANGLE_3D.sample_interior(rng), } } fn sample_boundary(&self, rng: &mut R) -> Self::Output { match self { Shape::Cuboid => CUBOID.sample_boundary(rng), Shape::Sphere => SPHERE.sample_boundary(rng), Shape::Capsule => CAPSULE_3D.sample_boundary(rng), Shape::Cylinder => CYLINDER.sample_boundary(rng), Shape::Tetrahedron => TETRAHEDRON.sample_boundary(rng), Shape::Triangle => TRIANGLE_3D.sample_boundary(rng), } } } impl Meshable for Shape { type Output = ShapeMeshBuilder; fn mesh(&self) -> Self::Output { ShapeMeshBuilder { shape: *self } } } impl MeshBuilder for ShapeMeshBuilder { fn build(&self) -> Mesh { match self.shape { Shape::Cuboid => CUBOID.mesh().into(), Shape::Sphere => SPHERE.mesh().into(), Shape::Capsule => CAPSULE_3D.mesh().into(), Shape::Cylinder => CYLINDER.mesh().into(), Shape::Tetrahedron => TETRAHEDRON.mesh().into(), Shape::Triangle => TRIANGLE_3D.mesh().into(), } } } /// 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 { interior: Handle, boundary: Handle, } /// Marker component for sampled points. #[derive(Component)] struct SamplePoint; /// Component for animating the spawn animation of lights. #[derive(Component)] struct SpawningPoint { progress: f32, } /// Marker component for lights which should change intensity. #[derive(Component)] struct DespawningPoint { progress: f32, } /// Marker component for lights which should change intensity. #[derive(Component)] struct FireflyLights; /// The pressed state of the mouse, used for camera motion. #[derive(Resource)] struct MousePressed(bool); /// Camera movement component. #[derive(Component)] struct CameraRig { /// Rotation around the vertical axis of the camera (radians). /// Positive changes makes the camera look more from the right. pub yaw: f32, /// Rotation around the horizontal axis of the camera (radians) (-pi/2; pi/2). /// Positive looks down from above. pub pitch: f32, /// Distance from the center, smaller distance causes more zoom. pub distance: f32, /// Location in 3D space at which the camera is looking and around which it is orbiting. pub target: Vec3, } fn setup( mut commands: Commands, mut meshes: ResMut>, mut materials: ResMut>, shapes: Res, ) { // Use seeded rng and store it in a resource; this makes the random output reproducible. let seeded_rng = ChaCha8Rng::seed_from_u64(4); // Chosen by a fair die roll, guaranteed to be random. commands.insert_resource(RandomSource(seeded_rng)); // Make a plane for establishing space. commands.spawn(( Mesh3d(meshes.add(Plane3d::default().mesh().size(20.0, 20.0))), MeshMaterial3d(materials.add(StandardMaterial { base_color: Color::srgb(0.3, 0.5, 0.3), perceptual_roughness: 0.95, metallic: 0.0, ..default() })), Transform::from_xyz(0.0, -2.5, 0.0), )); let shape_material = materials.add(StandardMaterial { base_color: Color::srgba(0.2, 0.1, 0.6, 0.3), reflectance: 0.0, alpha_mode: AlphaMode::Blend, cull_mode: None, ..default() }); // Spawn shapes to be sampled for (shape, translation) in shapes.0.iter() { // The sampled shape shown transparently: commands.spawn(( Mesh3d(meshes.add(shape.mesh())), MeshMaterial3d(shape_material.clone()), Transform::from_translation(*translation), )); // Lights which work as the bulk lighting of the fireflies: commands.spawn(( PointLight { range: 4.0, radius: 0.6, intensity: 1.0, shadows_enabled: false, color: Color::LinearRgba(INSIDE_POINT_COLOR), ..default() }, Transform::from_translation(*translation), FireflyLights, )); } // Global light: commands.spawn(( PointLight { color: SKY_COLOR, intensity: 2_000.0, shadows_enabled: false, ..default() }, Transform::from_xyz(4.0, 8.0, 4.0), )); // A camera: commands.spawn(( Camera3d::default(), Camera { hdr: true, // HDR is required for bloom clear_color: ClearColorConfig::Custom(SKY_COLOR), ..default() }, Tonemapping::TonyMcMapface, Transform::from_xyz(-2.0, 3.0, 5.0).looking_at(Vec3::ZERO, Vec3::Y), Bloom::NATURAL, CameraRig { yaw: 0.56, pitch: 0.45, distance: 8.0, target: Vec3::ZERO, }, )); // Store the mesh and material for sample points in resources: commands.insert_resource(PointMesh( meshes.add(Sphere::new(0.03).mesh().ico(1).unwrap()), )); commands.insert_resource(PointMaterial { interior: materials.add(StandardMaterial { base_color: Color::BLACK, reflectance: 0.05, emissive: 2.5 * INSIDE_POINT_COLOR, ..default() }), boundary: materials.add(StandardMaterial { base_color: Color::BLACK, reflectance: 0.05, emissive: 1.5 * BOUNDARY_POINT_COLOR, ..default() }), }); // Instructions for the example: commands.spawn(( Text::new( "Controls:\n\ M: Toggle between sampling boundary and interior.\n\ A: Toggle automatic spawning & despawning of points.\n\ R: Restart (erase all samples).\n\ S: Add one random sample.\n\ D: Add 100 random samples.\n\ Rotate camera by holding left mouse and panning.\n\ Zoom camera by scrolling via mouse or +/-.\n\ Move camera by L/R arrow keys.\n\ Tab: Toggle this text", ), Style { position_type: PositionType::Absolute, top: Val::Px(12.0), left: Val::Px(12.0), ..default() }, )); // No points are scheduled to spawn initially. commands.insert_resource(SpawnQueue(0)); // No points have been spawned initially. commands.insert_resource(PointCounter(0)); // The mode starts with interior points. commands.insert_resource(SamplingMode::Interior); // Points spawn automatically by default. commands.insert_resource(SpawningMode::Automatic); // 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, mut spawn_mode: ResMut, samples: Query>, shapes: Res, mut spawn_queue: ResMut, mut counter: ResMut, mut text_menus: Query<&mut Visibility, With>, mut camera_rig: Single<&mut CameraRig>, ) { // R => restart, deleting all samples if keyboard.just_pressed(KeyCode::KeyR) { // Don't forget to zero out the counter! counter.0 = 0; for entity in &samples { commands.entity(entity).despawn(); } } // S => sample once if keyboard.just_pressed(KeyCode::KeyS) { spawn_queue.0 += 1; } // D => sample a hundred if keyboard.just_pressed(KeyCode::KeyD) { spawn_queue.0 += 100; } // M => toggle mode between interior and boundary. if keyboard.just_pressed(KeyCode::KeyM) { match *mode { SamplingMode::Interior => *mode = SamplingMode::Boundary, SamplingMode::Boundary => *mode = SamplingMode::Interior, } } // A => toggle spawning mode between automatic and manual. if keyboard.just_pressed(KeyCode::KeyA) { match *spawn_mode { SpawningMode::Manual => *spawn_mode = SpawningMode::Automatic, SpawningMode::Automatic => *spawn_mode = SpawningMode::Manual, } } // Tab => toggle help menu. if keyboard.just_pressed(KeyCode::Tab) { for mut visibility in text_menus.iter_mut() { *visibility = match *visibility { Visibility::Hidden => Visibility::Visible, _ => Visibility::Hidden, }; } } // +/- => zoom camera. if keyboard.just_pressed(KeyCode::NumpadSubtract) || keyboard.just_pressed(KeyCode::Minus) { camera_rig.distance += MAX_CAMERA_DISTANCE / 15.0; camera_rig.distance = camera_rig .distance .clamp(MIN_CAMERA_DISTANCE, MAX_CAMERA_DISTANCE); } if keyboard.just_pressed(KeyCode::NumpadAdd) { camera_rig.distance -= MAX_CAMERA_DISTANCE / 15.0; camera_rig.distance = camera_rig .distance .clamp(MIN_CAMERA_DISTANCE, MAX_CAMERA_DISTANCE); } // Arrows => Move camera focus let left = keyboard.just_pressed(KeyCode::ArrowLeft); let right = keyboard.just_pressed(KeyCode::ArrowRight); if left || right { let mut closest = 0; let mut closest_distance = f32::MAX; for (i, (_, position)) in shapes.0.iter().enumerate() { let distance = camera_rig.target.distance(*position); if distance < closest_distance { closest = i; closest_distance = distance; } } if closest > 0 && left { camera_rig.target = shapes.0[closest - 1].1; } if closest < shapes.0.len() - 1 && right { camera_rig.target = shapes.0[closest + 1].1; } } } // Handle user mouse input for panning the camera around: fn handle_mouse( accumulated_mouse_motion: Res, accumulated_mouse_scroll: Res, mut button_events: EventReader, mut camera_rig: Single<&mut CameraRig>, 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 accumulated_mouse_scroll.delta != Vec2::ZERO { let mouse_scroll = accumulated_mouse_scroll.delta.y; camera_rig.distance -= mouse_scroll / 15.0 * MAX_CAMERA_DISTANCE; camera_rig.distance = camera_rig .distance .clamp(MIN_CAMERA_DISTANCE, MAX_CAMERA_DISTANCE); } // If the mouse is not pressed, just ignore motion events if !mouse_pressed.0 { return; } if accumulated_mouse_motion.delta != Vec2::ZERO { let displacement = accumulated_mouse_motion.delta; camera_rig.yaw += displacement.x / 90.; camera_rig.pitch += displacement.y / 90.; // The extra 0.01 is to disallow weird behaviour at the poles of the rotation camera_rig.pitch = camera_rig.pitch.clamp(-PI / 2.01, PI / 2.01); } } #[allow(clippy::too_many_arguments)] fn spawn_points( mut commands: Commands, mode: ResMut, shapes: Res, mut random_source: ResMut, sample_mesh: Res, sample_material: Res, mut spawn_queue: ResMut, mut counter: ResMut, spawn_mode: ResMut, ) { if let SpawningMode::Automatic = *spawn_mode { spawn_queue.0 += POINTS_PER_FRAME; } if spawn_queue.0 == 0 { return; } let rng = &mut random_source.0; // Don't go crazy for _ in 0..1000 { if spawn_queue.0 == 0 { break; } spawn_queue.0 -= 1; counter.0 += 1; let (shape, offset) = shapes.0.choose(rng).expect("There is at least one shape"); // Get a single random Vec3: let sample: Vec3 = *offset + match *mode { SamplingMode::Interior => shape.sample_interior(rng), SamplingMode::Boundary => shape.sample_boundary(rng), }; // Spawn a sphere at the random location: commands.spawn(( Mesh3d(sample_mesh.0.clone()), MeshMaterial3d(match *mode { SamplingMode::Interior => sample_material.interior.clone(), SamplingMode::Boundary => sample_material.boundary.clone(), }), Transform::from_translation(sample).with_scale(Vec3::ZERO), SamplePoint, SpawningPoint { progress: 0.0 }, )); } } fn despawn_points( mut commands: Commands, samples: Query>, spawn_mode: Res, mut counter: ResMut, mut random_source: ResMut, ) { // Do not despawn automatically in manual mode if let SpawningMode::Manual = *spawn_mode { return; } if counter.0 < MAX_POINTS { return; } let rng = &mut random_source.0; // Skip a random amount of points to ensure random despawning let skip = rng.gen_range(0..counter.0); let despawn_amount = (counter.0 - MAX_POINTS).min(100); counter.0 -= samples .iter() .skip(skip) .take(despawn_amount) .map(|entity| { commands .entity(entity) .insert(DespawningPoint { progress: 0.0 }) .remove::() .remove::(); }) .count(); } fn animate_spawning( mut commands: Commands, time: Res