bevy/examples/math/sampling_primitives.rs

706 lines
21 KiB
Rust
Raw Normal View History

//! This example shows how to sample random points from primitive shapes.
use std::f32::consts::PI;
use bevy::{
core_pipeline::{bloom::BloomSettings, tonemapping::Tonemapping},
input::mouse::{AccumulatedMouseMotion, AccumulatedMouseScroll, MouseButtonInput},
math::prelude::*,
prelude::*,
};
use rand::seq::SliceRandom;
use rand::{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,
}
Meshable extrusions (#13478) # Objective - Implement `Meshable` for `Extrusion<T>` ## Solution - `Meshable` requires `Meshable::Output: MeshBuilder` now. This means that all `some_primitive.mesh()` calls now return a `MeshBuilder`. These were added for primitives that did not have one prior to this. - A new trait `Extrudable: MeshBuilder` has been added. This trait allows you to specify the indices of the perimeter of the mesh created by this `MeshBuilder` and whether they are to be shaded smooth or flat. - `Extrusion<P: Primitive2d + Meshable>` is now `Meshable` aswell. The associated `MeshBuilder` is `ExtrusionMeshBuilder` which is generic over `P` and uses the `MeshBuilder` of its baseshape internally. - `ExtrusionMeshBuilder` exposes the configuration functions of its base-shapes builder. - Updated the `3d_shapes` example to include `Extrusion`s ## Migration Guide - Depending on the context, you may need to explicitly call `.mesh().build()` on primitives where you have previously called `.mesh()` - The `Output` type of custom `Meshable` implementations must now derive `MeshBuilder`. ## Additional information - The extrusions UVs are done so that - the front face (`+Z`) is in the area between `(0, 0)` and `(0.5, 0.5)`, - the back face (`-Z`) is in the area between `(0.5, 0)` and `(1, 0.5)` - the mantle is in the area between `(0, 0.5)` and `(1, 1)`. Each `PerimeterSegment` you specified in the `Extrudable` implementation will be allocated an equal portion of this area. - The UVs of the base shape are scaled to be in the front/back area so whatever method of filling the full UV-space the base shape used is how these areas will be filled. Here is an example of what that looks like on a capsule: https://github.com/bevyengine/bevy/assets/62256001/425ad288-fbbc-4634-9d3f-5e846cdce85f This is the texture used: ![extrusion uvs](https://github.com/bevyengine/bevy/assets/62256001/4e54e421-bfda-44b9-8571-412525cebddf) The `3d_shapes` example now looks like this: ![image_2024-05-22_235915753](https://github.com/bevyengine/bevy/assets/62256001/3d8bc86d-9ed1-47f2-899a-27aac0a265dd) --------- Co-authored-by: Lynn Büttgenbach <62256001+solis-lumine-vorago@users.noreply.github.com> Co-authored-by: Matty <weatherleymatthew@gmail.com> Co-authored-by: Matty <2975848+mweatherley@users.noreply.github.com>
2024-06-04 17:27:32 +00:00
struct ShapeMeshBuilder {
shape: Shape,
}
impl Shape {
/// Return a vector containing all implemented shapes
fn list_all_shapes() -> Vec<Shape> {
vec![
Shape::Cuboid,
Shape::Sphere,
Shape::Capsule,
Shape::Cylinder,
Shape::Tetrahedron,
Shape::Triangle,
]
}
}
impl ShapeSample for Shape {
type Output = Vec3;
fn sample_interior<R: Rng + ?Sized>(&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<R: Rng + ?Sized>(&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 {
Meshable extrusions (#13478) # Objective - Implement `Meshable` for `Extrusion<T>` ## Solution - `Meshable` requires `Meshable::Output: MeshBuilder` now. This means that all `some_primitive.mesh()` calls now return a `MeshBuilder`. These were added for primitives that did not have one prior to this. - A new trait `Extrudable: MeshBuilder` has been added. This trait allows you to specify the indices of the perimeter of the mesh created by this `MeshBuilder` and whether they are to be shaded smooth or flat. - `Extrusion<P: Primitive2d + Meshable>` is now `Meshable` aswell. The associated `MeshBuilder` is `ExtrusionMeshBuilder` which is generic over `P` and uses the `MeshBuilder` of its baseshape internally. - `ExtrusionMeshBuilder` exposes the configuration functions of its base-shapes builder. - Updated the `3d_shapes` example to include `Extrusion`s ## Migration Guide - Depending on the context, you may need to explicitly call `.mesh().build()` on primitives where you have previously called `.mesh()` - The `Output` type of custom `Meshable` implementations must now derive `MeshBuilder`. ## Additional information - The extrusions UVs are done so that - the front face (`+Z`) is in the area between `(0, 0)` and `(0.5, 0.5)`, - the back face (`-Z`) is in the area between `(0.5, 0)` and `(1, 0.5)` - the mantle is in the area between `(0, 0.5)` and `(1, 1)`. Each `PerimeterSegment` you specified in the `Extrudable` implementation will be allocated an equal portion of this area. - The UVs of the base shape are scaled to be in the front/back area so whatever method of filling the full UV-space the base shape used is how these areas will be filled. Here is an example of what that looks like on a capsule: https://github.com/bevyengine/bevy/assets/62256001/425ad288-fbbc-4634-9d3f-5e846cdce85f This is the texture used: ![extrusion uvs](https://github.com/bevyengine/bevy/assets/62256001/4e54e421-bfda-44b9-8571-412525cebddf) The `3d_shapes` example now looks like this: ![image_2024-05-22_235915753](https://github.com/bevyengine/bevy/assets/62256001/3d8bc86d-9ed1-47f2-899a-27aac0a265dd) --------- Co-authored-by: Lynn Büttgenbach <62256001+solis-lumine-vorago@users.noreply.github.com> Co-authored-by: Matty <weatherleymatthew@gmail.com> Co-authored-by: Matty <2975848+mweatherley@users.noreply.github.com>
2024-06-04 17:27:32 +00:00
type Output = ShapeMeshBuilder;
fn mesh(&self) -> Self::Output {
Meshable extrusions (#13478) # Objective - Implement `Meshable` for `Extrusion<T>` ## Solution - `Meshable` requires `Meshable::Output: MeshBuilder` now. This means that all `some_primitive.mesh()` calls now return a `MeshBuilder`. These were added for primitives that did not have one prior to this. - A new trait `Extrudable: MeshBuilder` has been added. This trait allows you to specify the indices of the perimeter of the mesh created by this `MeshBuilder` and whether they are to be shaded smooth or flat. - `Extrusion<P: Primitive2d + Meshable>` is now `Meshable` aswell. The associated `MeshBuilder` is `ExtrusionMeshBuilder` which is generic over `P` and uses the `MeshBuilder` of its baseshape internally. - `ExtrusionMeshBuilder` exposes the configuration functions of its base-shapes builder. - Updated the `3d_shapes` example to include `Extrusion`s ## Migration Guide - Depending on the context, you may need to explicitly call `.mesh().build()` on primitives where you have previously called `.mesh()` - The `Output` type of custom `Meshable` implementations must now derive `MeshBuilder`. ## Additional information - The extrusions UVs are done so that - the front face (`+Z`) is in the area between `(0, 0)` and `(0.5, 0.5)`, - the back face (`-Z`) is in the area between `(0.5, 0)` and `(1, 0.5)` - the mantle is in the area between `(0, 0.5)` and `(1, 1)`. Each `PerimeterSegment` you specified in the `Extrudable` implementation will be allocated an equal portion of this area. - The UVs of the base shape are scaled to be in the front/back area so whatever method of filling the full UV-space the base shape used is how these areas will be filled. Here is an example of what that looks like on a capsule: https://github.com/bevyengine/bevy/assets/62256001/425ad288-fbbc-4634-9d3f-5e846cdce85f This is the texture used: ![extrusion uvs](https://github.com/bevyengine/bevy/assets/62256001/4e54e421-bfda-44b9-8571-412525cebddf) The `3d_shapes` example now looks like this: ![image_2024-05-22_235915753](https://github.com/bevyengine/bevy/assets/62256001/3d8bc86d-9ed1-47f2-899a-27aac0a265dd) --------- Co-authored-by: Lynn Büttgenbach <62256001+solis-lumine-vorago@users.noreply.github.com> Co-authored-by: Matty <weatherleymatthew@gmail.com> Co-authored-by: Matty <2975848+mweatherley@users.noreply.github.com>
2024-06-04 17:27:32 +00:00
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(),
Meshable extrusions (#13478) # Objective - Implement `Meshable` for `Extrusion<T>` ## Solution - `Meshable` requires `Meshable::Output: MeshBuilder` now. This means that all `some_primitive.mesh()` calls now return a `MeshBuilder`. These were added for primitives that did not have one prior to this. - A new trait `Extrudable: MeshBuilder` has been added. This trait allows you to specify the indices of the perimeter of the mesh created by this `MeshBuilder` and whether they are to be shaded smooth or flat. - `Extrusion<P: Primitive2d + Meshable>` is now `Meshable` aswell. The associated `MeshBuilder` is `ExtrusionMeshBuilder` which is generic over `P` and uses the `MeshBuilder` of its baseshape internally. - `ExtrusionMeshBuilder` exposes the configuration functions of its base-shapes builder. - Updated the `3d_shapes` example to include `Extrusion`s ## Migration Guide - Depending on the context, you may need to explicitly call `.mesh().build()` on primitives where you have previously called `.mesh()` - The `Output` type of custom `Meshable` implementations must now derive `MeshBuilder`. ## Additional information - The extrusions UVs are done so that - the front face (`+Z`) is in the area between `(0, 0)` and `(0.5, 0.5)`, - the back face (`-Z`) is in the area between `(0.5, 0)` and `(1, 0.5)` - the mantle is in the area between `(0, 0.5)` and `(1, 1)`. Each `PerimeterSegment` you specified in the `Extrudable` implementation will be allocated an equal portion of this area. - The UVs of the base shape are scaled to be in the front/back area so whatever method of filling the full UV-space the base shape used is how these areas will be filled. Here is an example of what that looks like on a capsule: https://github.com/bevyengine/bevy/assets/62256001/425ad288-fbbc-4634-9d3f-5e846cdce85f This is the texture used: ![extrusion uvs](https://github.com/bevyengine/bevy/assets/62256001/4e54e421-bfda-44b9-8571-412525cebddf) The `3d_shapes` example now looks like this: ![image_2024-05-22_235915753](https://github.com/bevyengine/bevy/assets/62256001/3d8bc86d-9ed1-47f2-899a-27aac0a265dd) --------- Co-authored-by: Lynn Büttgenbach <62256001+solis-lumine-vorago@users.noreply.github.com> Co-authored-by: Matty <weatherleymatthew@gmail.com> Co-authored-by: Matty <2975848+mweatherley@users.noreply.github.com>
2024-06-04 17:27:32 +00:00
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<Mesh>);
/// A container for the handle storing the material used to display sampled points.
#[derive(Resource)]
struct PointMaterial {
interior: Handle<StandardMaterial>,
boundary: Handle<StandardMaterial>,
}
/// 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<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
shapes: Res<SampledShapes>,
) {
// 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(PbrBundle {
mesh: meshes.add(Plane3d::default().mesh().size(20.0, 20.0)),
material: materials.add(StandardMaterial {
base_color: Color::srgb(0.3, 0.5, 0.3),
perceptual_roughness: 0.95,
metallic: 0.0,
..default()
}),
transform: Transform::from_xyz(0.0, -2.5, 0.0),
..default()
});
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(PbrBundle {
mesh: meshes.add(shape.mesh()),
material: shape_material.clone(),
transform: Transform::from_translation(*translation),
..default()
});
// Lights which work as the bulk lighting of the fireflies:
commands.spawn((
PointLightBundle {
point_light: PointLight {
range: 4.0,
radius: 0.6,
intensity: 1.0,
shadows_enabled: false,
color: Color::LinearRgba(INSIDE_POINT_COLOR),
..default()
},
transform: Transform::from_translation(*translation),
..default()
},
FireflyLights,
));
}
// Global light:
commands.spawn(PointLightBundle {
point_light: PointLight {
color: SKY_COLOR,
intensity: 2_000.0,
shadows_enabled: false,
..default()
},
transform: Transform::from_xyz(4.0, 8.0, 4.0),
..default()
});
// A camera:
commands.spawn((
Camera3dBundle {
camera: Camera {
hdr: true, // HDR is required for bloom
clear_color: ClearColorConfig::Custom(SKY_COLOR),
..default()
},
tonemapping: Tonemapping::TonyMcMapface,
transform: Transform::from_xyz(-2.0, 3.0, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
..default()
},
BloomSettings::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(
TextBundle::from_section(
"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",
TextStyle::default(),
)
.with_style(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<ButtonInput<KeyCode>>,
mut mode: ResMut<SamplingMode>,
mut spawn_mode: ResMut<SpawningMode>,
samples: Query<Entity, With<SamplePoint>>,
shapes: Res<SampledShapes>,
mut spawn_queue: ResMut<SpawnQueue>,
mut counter: ResMut<PointCounter>,
mut text_menus: Query<&mut Visibility, With<Text>>,
mut camera: Query<&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,
};
}
}
let mut camera_rig = camera.single_mut();
// +/- => 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<AccumulatedMouseMotion>,
accumulated_mouse_scroll: Res<AccumulatedMouseScroll>,
mut button_events: EventReader<MouseButtonInput>,
mut camera: Query<&mut CameraRig>,
mut mouse_pressed: ResMut<MousePressed>,
) {
// 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());
}
let mut camera_rig = camera.single_mut();
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<SamplingMode>,
shapes: Res<SampledShapes>,
mut random_source: ResMut<RandomSource>,
sample_mesh: Res<PointMesh>,
sample_material: Res<PointMaterial>,
mut spawn_queue: ResMut<SpawnQueue>,
mut counter: ResMut<PointCounter>,
spawn_mode: ResMut<SpawningMode>,
) {
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((
PbrBundle {
mesh: sample_mesh.0.clone(),
material: match *mode {
SamplingMode::Interior => sample_material.interior.clone(),
SamplingMode::Boundary => sample_material.boundary.clone(),
},
transform: Transform::from_translation(sample).with_scale(Vec3::ZERO),
..default()
},
SamplePoint,
SpawningPoint { progress: 0.0 },
));
}
}
fn despawn_points(
mut commands: Commands,
samples: Query<Entity, With<SamplePoint>>,
spawn_mode: Res<SpawningMode>,
mut counter: ResMut<PointCounter>,
mut random_source: ResMut<RandomSource>,
) {
// 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::<SpawningPoint>()
.remove::<SamplePoint>();
})
.count();
}
fn animate_spawning(
mut commands: Commands,
time: Res<Time>,
mut samples: Query<(Entity, &mut Transform, &mut SpawningPoint)>,
) {
let dt = time.delta_seconds();
for (entity, mut transform, mut point) in samples.iter_mut() {
point.progress += dt / ANIMATION_TIME;
transform.scale = Vec3::splat(point.progress.min(1.0));
if point.progress >= 1.0 {
commands.entity(entity).remove::<SpawningPoint>();
}
}
}
fn animate_despawning(
mut commands: Commands,
time: Res<Time>,
mut samples: Query<(Entity, &mut Transform, &mut DespawningPoint)>,
) {
let dt = time.delta_seconds();
for (entity, mut transform, mut point) in samples.iter_mut() {
point.progress += dt / ANIMATION_TIME;
// If the point is already smaller than expected, jump ahead with the despawning progress to avoid sudden jumps in size
point.progress = f32::max(point.progress, 1.0 - transform.scale.x);
transform.scale = Vec3::splat((1.0 - point.progress).max(0.0));
if point.progress >= 1.0 {
commands.entity(entity).despawn();
}
}
}
fn update_camera(mut camera: Query<(&mut Transform, &CameraRig), Changed<CameraRig>>) {
for (mut transform, rig) in camera.iter_mut() {
let looking_direction =
Quat::from_rotation_y(-rig.yaw) * Quat::from_rotation_x(rig.pitch) * Vec3::Z;
transform.translation = rig.target - rig.distance * looking_direction;
transform.look_at(rig.target, Dir3::Y);
}
}
fn update_lights(
mut lights: Query<&mut PointLight, With<FireflyLights>>,
counter: Res<PointCounter>,
) {
let saturation = (counter.0 as f32 / MAX_POINTS as f32).min(2.0);
let intensity = 40_000.0 * saturation;
for mut light in lights.iter_mut() {
light.intensity = light.intensity.lerp(intensity, 0.04);
}
}