bevy/examples/math/sampling_primitives.rs
Pablo Reinhardt d96a9d15f6
Migrate from Query::single and friends to Single (#15872)
# Objective

- closes #15866

## Solution

- Simply migrate where possible.

## Testing

- Expect that CI will do most of the work. Examples is another way of
testing this, as most of the work is in that area.
---

## Notes
For now, this PR doesn't migrate `QueryState::single` and friends as for
now, this look like another issue. So for example, QueryBuilders that
used single or `World::query` that used single wasn't migrated. If there
is a easy way to migrate those, please let me know.

Most of the uses of `Query::single` were removed, the only other uses
that I found was related to tests of said methods, so will probably be
removed when we remove `Query::single`.
2024-10-13 20:32:06 +00:00

688 lines
21 KiB
Rust

//! 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<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 {
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<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((
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<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_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<AccumulatedMouseMotion>,
accumulated_mouse_scroll: Res<AccumulatedMouseScroll>,
mut button_events: EventReader<MouseButtonInput>,
mut camera_rig: Single<&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());
}
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((
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<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);
}
}