Vendor dependencies for 0.3.0 release

This commit is contained in:
2025-09-27 10:29:08 -05:00
parent 0c8d39d483
commit 82ab7f317b
26803 changed files with 16134934 additions and 0 deletions

420
vendor/bevy/examples/math/bounding_2d.rs vendored Normal file
View File

@@ -0,0 +1,420 @@
//! This example demonstrates bounding volume intersections.
use bevy::{
color::palettes::css::*,
math::{bounding::*, ops, Isometry2d},
prelude::*,
};
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.init_state::<Test>()
.add_systems(Startup, setup)
.add_systems(
Update,
(update_text, spin, update_volumes, update_test_state),
)
.add_systems(
PostUpdate,
(
render_shapes,
(
aabb_intersection_system.run_if(in_state(Test::AabbSweep)),
circle_intersection_system.run_if(in_state(Test::CircleSweep)),
ray_cast_system.run_if(in_state(Test::RayCast)),
aabb_cast_system.run_if(in_state(Test::AabbCast)),
bounding_circle_cast_system.run_if(in_state(Test::CircleCast)),
),
render_volumes,
)
.chain(),
)
.run();
}
#[derive(Component)]
struct Spin;
fn spin(time: Res<Time>, mut query: Query<&mut Transform, With<Spin>>) {
for mut transform in query.iter_mut() {
transform.rotation *= Quat::from_rotation_z(time.delta_secs() / 5.);
}
}
#[derive(States, Default, Debug, Hash, PartialEq, Eq, Clone, Copy)]
enum Test {
AabbSweep,
CircleSweep,
#[default]
RayCast,
AabbCast,
CircleCast,
}
fn update_test_state(
keycode: Res<ButtonInput<KeyCode>>,
cur_state: Res<State<Test>>,
mut state: ResMut<NextState<Test>>,
) {
if !keycode.just_pressed(KeyCode::Space) {
return;
}
use Test::*;
let next = match **cur_state {
AabbSweep => CircleSweep,
CircleSweep => RayCast,
RayCast => AabbCast,
AabbCast => CircleCast,
CircleCast => AabbSweep,
};
state.set(next);
}
fn update_text(mut text: Single<&mut Text>, cur_state: Res<State<Test>>) {
if !cur_state.is_changed() {
return;
}
text.clear();
text.push_str("Intersection test:\n");
use Test::*;
for &test in &[AabbSweep, CircleSweep, RayCast, AabbCast, CircleCast] {
let s = if **cur_state == test { "*" } else { " " };
text.push_str(&format!(" {s} {test:?} {s}\n"));
}
text.push_str("\nPress space to cycle");
}
#[derive(Component)]
enum Shape {
Rectangle(Rectangle),
Circle(Circle),
Triangle(Triangle2d),
Line(Segment2d),
Capsule(Capsule2d),
Polygon(RegularPolygon),
}
fn render_shapes(mut gizmos: Gizmos, query: Query<(&Shape, &Transform)>) {
let color = GRAY;
for (shape, transform) in query.iter() {
let translation = transform.translation.xy();
let rotation = transform.rotation.to_euler(EulerRot::YXZ).2;
let isometry = Isometry2d::new(translation, Rot2::radians(rotation));
match shape {
Shape::Rectangle(r) => {
gizmos.primitive_2d(r, isometry, color);
}
Shape::Circle(c) => {
gizmos.primitive_2d(c, isometry, color);
}
Shape::Triangle(t) => {
gizmos.primitive_2d(t, isometry, color);
}
Shape::Line(l) => {
gizmos.primitive_2d(l, isometry, color);
}
Shape::Capsule(c) => {
gizmos.primitive_2d(c, isometry, color);
}
Shape::Polygon(p) => {
gizmos.primitive_2d(p, isometry, color);
}
}
}
}
#[derive(Component)]
enum DesiredVolume {
Aabb,
Circle,
}
#[derive(Component, Debug)]
enum CurrentVolume {
Aabb(Aabb2d),
Circle(BoundingCircle),
}
fn update_volumes(
mut commands: Commands,
query: Query<
(Entity, &DesiredVolume, &Shape, &Transform),
Or<(Changed<DesiredVolume>, Changed<Shape>, Changed<Transform>)>,
>,
) {
for (entity, desired_volume, shape, transform) in query.iter() {
let translation = transform.translation.xy();
let rotation = transform.rotation.to_euler(EulerRot::YXZ).2;
let isometry = Isometry2d::new(translation, Rot2::radians(rotation));
match desired_volume {
DesiredVolume::Aabb => {
let aabb = match shape {
Shape::Rectangle(r) => r.aabb_2d(isometry),
Shape::Circle(c) => c.aabb_2d(isometry),
Shape::Triangle(t) => t.aabb_2d(isometry),
Shape::Line(l) => l.aabb_2d(isometry),
Shape::Capsule(c) => c.aabb_2d(isometry),
Shape::Polygon(p) => p.aabb_2d(isometry),
};
commands.entity(entity).insert(CurrentVolume::Aabb(aabb));
}
DesiredVolume::Circle => {
let circle = match shape {
Shape::Rectangle(r) => r.bounding_circle(isometry),
Shape::Circle(c) => c.bounding_circle(isometry),
Shape::Triangle(t) => t.bounding_circle(isometry),
Shape::Line(l) => l.bounding_circle(isometry),
Shape::Capsule(c) => c.bounding_circle(isometry),
Shape::Polygon(p) => p.bounding_circle(isometry),
};
commands
.entity(entity)
.insert(CurrentVolume::Circle(circle));
}
}
}
}
fn render_volumes(mut gizmos: Gizmos, query: Query<(&CurrentVolume, &Intersects)>) {
for (volume, intersects) in query.iter() {
let color = if **intersects { AQUA } else { ORANGE_RED };
match volume {
CurrentVolume::Aabb(a) => {
gizmos.rect_2d(a.center(), a.half_size() * 2., color);
}
CurrentVolume::Circle(c) => {
gizmos.circle_2d(c.center(), c.radius(), color);
}
}
}
}
#[derive(Component, Deref, DerefMut, Default)]
struct Intersects(bool);
const OFFSET_X: f32 = 125.;
const OFFSET_Y: f32 = 75.;
fn setup(mut commands: Commands) {
commands.spawn(Camera2d);
commands.spawn((
Transform::from_xyz(-OFFSET_X, OFFSET_Y, 0.),
Shape::Circle(Circle::new(45.)),
DesiredVolume::Aabb,
Intersects::default(),
));
commands.spawn((
Transform::from_xyz(0., OFFSET_Y, 0.),
Shape::Rectangle(Rectangle::new(80., 80.)),
Spin,
DesiredVolume::Circle,
Intersects::default(),
));
commands.spawn((
Transform::from_xyz(OFFSET_X, OFFSET_Y, 0.),
Shape::Triangle(Triangle2d::new(
Vec2::new(-40., -40.),
Vec2::new(-20., 40.),
Vec2::new(40., 50.),
)),
Spin,
DesiredVolume::Aabb,
Intersects::default(),
));
commands.spawn((
Transform::from_xyz(-OFFSET_X, -OFFSET_Y, 0.),
Shape::Line(Segment2d::from_direction_and_length(
Dir2::from_xy(1., 0.3).unwrap(),
90.,
)),
Spin,
DesiredVolume::Circle,
Intersects::default(),
));
commands.spawn((
Transform::from_xyz(0., -OFFSET_Y, 0.),
Shape::Capsule(Capsule2d::new(25., 50.)),
Spin,
DesiredVolume::Aabb,
Intersects::default(),
));
commands.spawn((
Transform::from_xyz(OFFSET_X, -OFFSET_Y, 0.),
Shape::Polygon(RegularPolygon::new(50., 6)),
Spin,
DesiredVolume::Circle,
Intersects::default(),
));
commands.spawn((
Text::default(),
Node {
position_type: PositionType::Absolute,
top: Val::Px(12.0),
left: Val::Px(12.0),
..default()
},
));
}
fn draw_filled_circle(gizmos: &mut Gizmos, position: Vec2, color: Srgba) {
for r in [1., 2., 3.] {
gizmos.circle_2d(position, r, color);
}
}
fn draw_ray(gizmos: &mut Gizmos, ray: &RayCast2d) {
gizmos.line_2d(
ray.ray.origin,
ray.ray.origin + *ray.ray.direction * ray.max,
WHITE,
);
draw_filled_circle(gizmos, ray.ray.origin, FUCHSIA);
}
fn get_and_draw_ray(gizmos: &mut Gizmos, time: &Time) -> RayCast2d {
let ray = Vec2::new(ops::cos(time.elapsed_secs()), ops::sin(time.elapsed_secs()));
let dist = 150. + ops::sin(0.5 * time.elapsed_secs()).abs() * 500.;
let aabb_ray = Ray2d {
origin: ray * 250.,
direction: Dir2::new_unchecked(-ray),
};
let ray_cast = RayCast2d::from_ray(aabb_ray, dist - 20.);
draw_ray(gizmos, &ray_cast);
ray_cast
}
fn ray_cast_system(
mut gizmos: Gizmos,
time: Res<Time>,
mut volumes: Query<(&CurrentVolume, &mut Intersects)>,
) {
let ray_cast = get_and_draw_ray(&mut gizmos, &time);
for (volume, mut intersects) in volumes.iter_mut() {
let toi = match volume {
CurrentVolume::Aabb(a) => ray_cast.aabb_intersection_at(a),
CurrentVolume::Circle(c) => ray_cast.circle_intersection_at(c),
};
**intersects = toi.is_some();
if let Some(toi) = toi {
draw_filled_circle(
&mut gizmos,
ray_cast.ray.origin + *ray_cast.ray.direction * toi,
LIME,
);
}
}
}
fn aabb_cast_system(
mut gizmos: Gizmos,
time: Res<Time>,
mut volumes: Query<(&CurrentVolume, &mut Intersects)>,
) {
let ray_cast = get_and_draw_ray(&mut gizmos, &time);
let aabb_cast = AabbCast2d {
aabb: Aabb2d::new(Vec2::ZERO, Vec2::splat(15.)),
ray: ray_cast,
};
for (volume, mut intersects) in volumes.iter_mut() {
let toi = match *volume {
CurrentVolume::Aabb(a) => aabb_cast.aabb_collision_at(a),
CurrentVolume::Circle(_) => None,
};
**intersects = toi.is_some();
if let Some(toi) = toi {
gizmos.rect_2d(
aabb_cast.ray.ray.origin + *aabb_cast.ray.ray.direction * toi,
aabb_cast.aabb.half_size() * 2.,
LIME,
);
}
}
}
fn bounding_circle_cast_system(
mut gizmos: Gizmos,
time: Res<Time>,
mut volumes: Query<(&CurrentVolume, &mut Intersects)>,
) {
let ray_cast = get_and_draw_ray(&mut gizmos, &time);
let circle_cast = BoundingCircleCast {
circle: BoundingCircle::new(Vec2::ZERO, 15.),
ray: ray_cast,
};
for (volume, mut intersects) in volumes.iter_mut() {
let toi = match *volume {
CurrentVolume::Aabb(_) => None,
CurrentVolume::Circle(c) => circle_cast.circle_collision_at(c),
};
**intersects = toi.is_some();
if let Some(toi) = toi {
gizmos.circle_2d(
circle_cast.ray.ray.origin + *circle_cast.ray.ray.direction * toi,
circle_cast.circle.radius(),
LIME,
);
}
}
}
fn get_intersection_position(time: &Time) -> Vec2 {
let x = ops::cos(0.8 * time.elapsed_secs()) * 250.;
let y = ops::sin(0.4 * time.elapsed_secs()) * 100.;
Vec2::new(x, y)
}
fn aabb_intersection_system(
mut gizmos: Gizmos,
time: Res<Time>,
mut volumes: Query<(&CurrentVolume, &mut Intersects)>,
) {
let center = get_intersection_position(&time);
let aabb = Aabb2d::new(center, Vec2::splat(50.));
gizmos.rect_2d(center, aabb.half_size() * 2., YELLOW);
for (volume, mut intersects) in volumes.iter_mut() {
let hit = match volume {
CurrentVolume::Aabb(a) => aabb.intersects(a),
CurrentVolume::Circle(c) => aabb.intersects(c),
};
**intersects = hit;
}
}
fn circle_intersection_system(
mut gizmos: Gizmos,
time: Res<Time>,
mut volumes: Query<(&CurrentVolume, &mut Intersects)>,
) {
let center = get_intersection_position(&time);
let circle = BoundingCircle::new(center, 50.);
gizmos.circle_2d(center, circle.radius(), YELLOW);
for (volume, mut intersects) in volumes.iter_mut() {
let hit = match volume {
CurrentVolume::Aabb(a) => circle.intersects(a),
CurrentVolume::Circle(c) => circle.intersects(c),
};
**intersects = hit;
}
}

View File

@@ -0,0 +1,419 @@
//! This example exhibits different available modes of constructing cubic Bezier curves.
use bevy::{
app::{App, Startup, Update},
color::*,
ecs::system::Commands,
gizmos::gizmos::Gizmos,
input::{mouse::MouseButtonInput, ButtonState},
math::{cubic_splines::*, vec2},
prelude::*,
};
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Startup, setup)
.add_systems(
Update,
(
handle_keypress,
handle_mouse_move,
handle_mouse_press,
draw_edit_move,
update_curve,
update_spline_mode_text,
update_cycling_mode_text,
draw_curve,
draw_control_points,
)
.chain(),
)
.run();
}
fn setup(mut commands: Commands) {
// Initialize the modes with their defaults:
let spline_mode = SplineMode::default();
commands.insert_resource(spline_mode);
let cycling_mode = CyclingMode::default();
commands.insert_resource(cycling_mode);
// Starting data for [`ControlPoints`]:
let default_points = vec![
vec2(-500., -200.),
vec2(-250., 250.),
vec2(250., 250.),
vec2(500., -200.),
];
let default_tangents = vec![
vec2(0., 200.),
vec2(200., 0.),
vec2(0., -200.),
vec2(-200., 0.),
];
let default_control_data = ControlPoints {
points_and_tangents: default_points.into_iter().zip(default_tangents).collect(),
};
let curve = form_curve(&default_control_data, spline_mode, cycling_mode);
commands.insert_resource(curve);
commands.insert_resource(default_control_data);
// Mouse tracking information:
commands.insert_resource(MousePosition::default());
commands.insert_resource(MouseEditMove::default());
commands.spawn(Camera2d);
// The instructions and modes are rendered on the left-hand side in a column.
let instructions_text = "Click and drag to add control points and their tangents\n\
R: Remove the last control point\n\
S: Cycle the spline construction being used\n\
C: Toggle cyclic curve construction";
let spline_mode_text = format!("Spline: {spline_mode}");
let cycling_mode_text = format!("{cycling_mode}");
let style = TextFont::default();
commands
.spawn(Node {
position_type: PositionType::Absolute,
top: Val::Px(12.0),
left: Val::Px(12.0),
flex_direction: FlexDirection::Column,
row_gap: Val::Px(20.0),
..default()
})
.with_children(|parent| {
parent.spawn((Text::new(instructions_text), style.clone()));
parent.spawn((SplineModeText, Text(spline_mode_text), style.clone()));
parent.spawn((CyclingModeText, Text(cycling_mode_text), style.clone()));
});
}
// -----------------------------------
// Curve-related Resources and Systems
// -----------------------------------
/// The current spline mode, which determines the spline method used in conjunction with the
/// control points.
#[derive(Clone, Copy, Resource, Default)]
enum SplineMode {
#[default]
Hermite,
Cardinal,
B,
}
impl std::fmt::Display for SplineMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SplineMode::Hermite => f.write_str("Hermite"),
SplineMode::Cardinal => f.write_str("Cardinal"),
SplineMode::B => f.write_str("B"),
}
}
}
/// The current cycling mode, which determines whether the control points should be interpolated
/// cyclically (to make a loop).
#[derive(Clone, Copy, Resource, Default)]
enum CyclingMode {
#[default]
NotCyclic,
Cyclic,
}
impl std::fmt::Display for CyclingMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CyclingMode::NotCyclic => f.write_str("Not Cyclic"),
CyclingMode::Cyclic => f.write_str("Cyclic"),
}
}
}
/// The curve presently being displayed. This is optional because there may not be enough control
/// points to actually generate a curve.
#[derive(Clone, Default, Resource)]
struct Curve(Option<CubicCurve<Vec2>>);
/// The control points used to generate a curve. The tangent components are only used in the case of
/// Hermite interpolation.
#[derive(Clone, Resource)]
struct ControlPoints {
points_and_tangents: Vec<(Vec2, Vec2)>,
}
/// This system is responsible for updating the [`Curve`] when the [control points] or active modes
/// change.
///
/// [control points]: ControlPoints
fn update_curve(
control_points: Res<ControlPoints>,
spline_mode: Res<SplineMode>,
cycling_mode: Res<CyclingMode>,
mut curve: ResMut<Curve>,
) {
if !control_points.is_changed() && !spline_mode.is_changed() && !cycling_mode.is_changed() {
return;
}
*curve = form_curve(&control_points, *spline_mode, *cycling_mode);
}
/// This system uses gizmos to draw the current [`Curve`] by breaking it up into a large number
/// of line segments.
fn draw_curve(curve: Res<Curve>, mut gizmos: Gizmos) {
let Some(ref curve) = curve.0 else {
return;
};
// Scale resolution with curve length so it doesn't degrade as the length increases.
let resolution = 100 * curve.segments().len();
gizmos.linestrip(
curve.iter_positions(resolution).map(|pt| pt.extend(0.0)),
Color::srgb(1.0, 1.0, 1.0),
);
}
/// This system uses gizmos to draw the current [control points] as circles, displaying their
/// tangent vectors as arrows in the case of a Hermite spline.
///
/// [control points]: ControlPoints
fn draw_control_points(
control_points: Res<ControlPoints>,
spline_mode: Res<SplineMode>,
mut gizmos: Gizmos,
) {
for &(point, tangent) in &control_points.points_and_tangents {
gizmos.circle_2d(point, 10.0, Color::srgb(0.0, 1.0, 0.0));
if matches!(*spline_mode, SplineMode::Hermite) {
gizmos.arrow_2d(point, point + tangent, Color::srgb(1.0, 0.0, 0.0));
}
}
}
/// Helper function for generating a [`Curve`] from [control points] and selected modes.
///
/// [control points]: ControlPoints
fn form_curve(
control_points: &ControlPoints,
spline_mode: SplineMode,
cycling_mode: CyclingMode,
) -> Curve {
let (points, tangents): (Vec<_>, Vec<_>) =
control_points.points_and_tangents.iter().copied().unzip();
match spline_mode {
SplineMode::Hermite => {
let spline = CubicHermite::new(points, tangents);
Curve(match cycling_mode {
CyclingMode::NotCyclic => spline.to_curve().ok(),
CyclingMode::Cyclic => spline.to_curve_cyclic().ok(),
})
}
SplineMode::Cardinal => {
let spline = CubicCardinalSpline::new_catmull_rom(points);
Curve(match cycling_mode {
CyclingMode::NotCyclic => spline.to_curve().ok(),
CyclingMode::Cyclic => spline.to_curve_cyclic().ok(),
})
}
SplineMode::B => {
let spline = CubicBSpline::new(points);
Curve(match cycling_mode {
CyclingMode::NotCyclic => spline.to_curve().ok(),
CyclingMode::Cyclic => spline.to_curve_cyclic().ok(),
})
}
}
}
// --------------------
// Text-related Components and Systems
// --------------------
/// Marker component for the text node that displays the current [`SplineMode`].
#[derive(Component)]
struct SplineModeText;
/// Marker component for the text node that displays the current [`CyclingMode`].
#[derive(Component)]
struct CyclingModeText;
fn update_spline_mode_text(
spline_mode: Res<SplineMode>,
mut spline_mode_text: Query<&mut Text, With<SplineModeText>>,
) {
if !spline_mode.is_changed() {
return;
}
let new_text = format!("Spline: {}", *spline_mode);
for mut spline_mode_text in spline_mode_text.iter_mut() {
(**spline_mode_text).clone_from(&new_text);
}
}
fn update_cycling_mode_text(
cycling_mode: Res<CyclingMode>,
mut cycling_mode_text: Query<&mut Text, With<CyclingModeText>>,
) {
if !cycling_mode.is_changed() {
return;
}
let new_text = format!("{}", *cycling_mode);
for mut cycling_mode_text in cycling_mode_text.iter_mut() {
(**cycling_mode_text).clone_from(&new_text);
}
}
// -----------------------------------
// Input-related Resources and Systems
// -----------------------------------
/// A small state machine which tracks a click-and-drag motion used to create new control points.
///
/// When the user is not doing a click-and-drag motion, the `start` field is `None`. When the user
/// presses the left mouse button, the location of that press is temporarily stored in the field.
#[derive(Clone, Default, Resource)]
struct MouseEditMove {
start: Option<Vec2>,
}
/// The current mouse position, if known.
#[derive(Clone, Default, Resource)]
struct MousePosition(Option<Vec2>);
/// Update the current cursor position and track it in the [`MousePosition`] resource.
fn handle_mouse_move(
mut cursor_events: EventReader<CursorMoved>,
mut mouse_position: ResMut<MousePosition>,
) {
if let Some(cursor_event) = cursor_events.read().last() {
mouse_position.0 = Some(cursor_event.position);
}
}
/// This system handles updating the [`MouseEditMove`] resource, orchestrating the logical part
/// of the click-and-drag motion which actually creates new control points.
fn handle_mouse_press(
mut button_events: EventReader<MouseButtonInput>,
mouse_position: Res<MousePosition>,
mut edit_move: ResMut<MouseEditMove>,
mut control_points: ResMut<ControlPoints>,
camera: Single<(&Camera, &GlobalTransform)>,
) {
let Some(mouse_pos) = mouse_position.0 else {
return;
};
// Handle click and drag behavior
for button_event in button_events.read() {
if button_event.button != MouseButton::Left {
continue;
}
match button_event.state {
ButtonState::Pressed => {
if edit_move.start.is_some() {
// If the edit move already has a start, press event should do nothing.
continue;
}
// This press represents the start of the edit move.
edit_move.start = Some(mouse_pos);
}
ButtonState::Released => {
// Release is only meaningful if we started an edit move.
let Some(start) = edit_move.start else {
continue;
};
let (camera, camera_transform) = *camera;
// Convert the starting point and end point (current mouse pos) into world coords:
let Ok(point) = camera.viewport_to_world_2d(camera_transform, start) else {
continue;
};
let Ok(end_point) = camera.viewport_to_world_2d(camera_transform, mouse_pos) else {
continue;
};
let tangent = end_point - point;
// The start of the click-and-drag motion represents the point to add,
// while the difference with the current position represents the tangent.
control_points.points_and_tangents.push((point, tangent));
// Reset the edit move since we've consumed it.
edit_move.start = None;
}
}
}
}
/// This system handles drawing the "preview" control point based on the state of [`MouseEditMove`].
fn draw_edit_move(
edit_move: Res<MouseEditMove>,
mouse_position: Res<MousePosition>,
mut gizmos: Gizmos,
camera: Single<(&Camera, &GlobalTransform)>,
) {
let Some(start) = edit_move.start else {
return;
};
let Some(mouse_pos) = mouse_position.0 else {
return;
};
let (camera, camera_transform) = *camera;
// Resources store data in viewport coordinates, so we need to convert to world coordinates
// to display them:
let Ok(start) = camera.viewport_to_world_2d(camera_transform, start) else {
return;
};
let Ok(end) = camera.viewport_to_world_2d(camera_transform, mouse_pos) else {
return;
};
gizmos.circle_2d(start, 10.0, Color::srgb(0.0, 1.0, 0.7));
gizmos.circle_2d(start, 7.0, Color::srgb(0.0, 1.0, 0.7));
gizmos.arrow_2d(start, end, Color::srgb(1.0, 0.0, 0.7));
}
/// This system handles all keyboard commands.
fn handle_keypress(
keyboard: Res<ButtonInput<KeyCode>>,
mut spline_mode: ResMut<SplineMode>,
mut cycling_mode: ResMut<CyclingMode>,
mut control_points: ResMut<ControlPoints>,
) {
// S => change spline mode
if keyboard.just_pressed(KeyCode::KeyS) {
*spline_mode = match *spline_mode {
SplineMode::Hermite => SplineMode::Cardinal,
SplineMode::Cardinal => SplineMode::B,
SplineMode::B => SplineMode::Hermite,
}
}
// C => change cycling mode
if keyboard.just_pressed(KeyCode::KeyC) {
*cycling_mode = match *cycling_mode {
CyclingMode::NotCyclic => CyclingMode::Cyclic,
CyclingMode::Cyclic => CyclingMode::NotCyclic,
}
}
// R => remove last control point
if keyboard.just_pressed(KeyCode::KeyR) {
control_points.points_and_tangents.pop();
}
}

View File

@@ -0,0 +1,481 @@
//! This example demonstrates how you can add your own custom primitives to bevy highlighting
//! traits you may want to implement for your primitives to achieve different functionalities.
use std::f32::consts::{PI, SQRT_2};
use bevy::{
color::palettes::css::{RED, WHITE},
input::common_conditions::input_just_pressed,
math::{
bounding::{
Aabb2d, Bounded2d, Bounded3d, BoundedExtrusion, BoundingCircle, BoundingVolume,
},
Isometry2d,
},
prelude::*,
render::{
camera::ScalingMode,
mesh::{Extrudable, ExtrusionBuilder, PerimeterSegment},
render_asset::RenderAssetUsages,
},
};
const HEART: Heart = Heart::new(0.5);
const EXTRUSION: Extrusion<Heart> = Extrusion {
base_shape: Heart::new(0.5),
half_depth: 0.5,
};
// The transform of the camera in 2D
const TRANSFORM_2D: Transform = Transform {
translation: Vec3::ZERO,
rotation: Quat::IDENTITY,
scale: Vec3::ONE,
};
// The projection used for the camera in 2D
const PROJECTION_2D: Projection = Projection::Orthographic(OrthographicProjection {
near: -1.0,
far: 10.0,
scale: 1.0,
viewport_origin: Vec2::new(0.5, 0.5),
scaling_mode: ScalingMode::AutoMax {
max_width: 8.0,
max_height: 20.0,
},
area: Rect {
min: Vec2::NEG_ONE,
max: Vec2::ONE,
},
});
// The transform of the camera in 3D
const TRANSFORM_3D: Transform = Transform {
translation: Vec3::ZERO,
// The camera is pointing at the 3D shape
rotation: Quat::from_xyzw(-0.14521316, -0.0, -0.0, 0.98940045),
scale: Vec3::ONE,
};
// The projection used for the camera in 3D
const PROJECTION_3D: Projection = Projection::Perspective(PerspectiveProjection {
fov: PI / 4.0,
near: 0.1,
far: 1000.0,
aspect_ratio: 1.0,
});
/// State for tracking the currently displayed shape
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, States, Default, Reflect)]
enum CameraActive {
#[default]
/// The 2D shape is displayed
Dim2,
/// The 3D shape is displayed
Dim3,
}
/// State for tracking the currently displayed shape
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, States, Default, Reflect)]
enum BoundingShape {
#[default]
/// No bounding shapes
None,
/// The bounding sphere or circle of the shape
BoundingSphere,
/// The Axis Aligned Bounding Box (AABB) of the shape
BoundingBox,
}
/// A marker component for our 2D shapes so we can query them separately from the camera
#[derive(Component)]
struct Shape2d;
/// A marker component for our 3D shapes so we can query them separately from the camera
#[derive(Component)]
struct Shape3d;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.init_state::<BoundingShape>()
.init_state::<CameraActive>()
.add_systems(Startup, setup)
.add_systems(
Update,
(
(rotate_2d_shapes, bounding_shapes_2d).run_if(in_state(CameraActive::Dim2)),
(rotate_3d_shapes, bounding_shapes_3d).run_if(in_state(CameraActive::Dim3)),
update_bounding_shape.run_if(input_just_pressed(KeyCode::KeyB)),
switch_cameras.run_if(input_just_pressed(KeyCode::Space)),
),
)
.run();
}
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
// Spawn the camera
commands.spawn((Camera3d::default(), TRANSFORM_2D, PROJECTION_2D));
// Spawn the 2D heart
commands.spawn((
// We can use the methods defined on the `MeshBuilder` to customize the mesh.
Mesh3d(meshes.add(HEART.mesh().resolution(50))),
MeshMaterial3d(materials.add(StandardMaterial {
emissive: RED.into(),
base_color: RED.into(),
..Default::default()
})),
Transform::from_xyz(0.0, 0.0, 0.0),
Shape2d,
));
// Spawn an extrusion of the heart.
commands.spawn((
// We can set a custom resolution for the round parts of the extrusion as well.
Mesh3d(meshes.add(EXTRUSION.mesh().resolution(50))),
MeshMaterial3d(materials.add(StandardMaterial {
base_color: RED.into(),
..Default::default()
})),
Transform::from_xyz(0., -3., -10.).with_rotation(Quat::from_rotation_x(-PI / 4.)),
Shape3d,
));
// Point light for 3D
commands.spawn((
PointLight {
shadows_enabled: true,
intensity: 10_000_000.,
range: 100.0,
shadow_depth_bias: 0.2,
..default()
},
Transform::from_xyz(8.0, 12.0, 1.0),
));
// Example instructions
commands.spawn((
Text::new("Press 'B' to toggle between no bounding shapes, bounding boxes (AABBs) and bounding spheres / circles\n\
Press 'Space' to switch between 3D and 2D"),
Node {
position_type: PositionType::Absolute,
top: Val::Px(12.0),
left: Val::Px(12.0),
..default()
},
));
}
// Rotate the 2D shapes.
fn rotate_2d_shapes(mut shapes: Query<&mut Transform, With<Shape2d>>, time: Res<Time>) {
let elapsed_seconds = time.elapsed_secs();
for mut transform in shapes.iter_mut() {
transform.rotation = Quat::from_rotation_z(elapsed_seconds);
}
}
// Draw bounding boxes or circles for the 2D shapes.
fn bounding_shapes_2d(
shapes: Query<&Transform, With<Shape2d>>,
mut gizmos: Gizmos,
bounding_shape: Res<State<BoundingShape>>,
) {
for transform in shapes.iter() {
// Get the rotation angle from the 3D rotation.
let rotation = transform.rotation.to_scaled_axis().z;
let rotation = Rot2::radians(rotation);
let isometry = Isometry2d::new(transform.translation.xy(), rotation);
match bounding_shape.get() {
BoundingShape::None => (),
BoundingShape::BoundingBox => {
// Get the AABB of the primitive with the rotation and translation of the mesh.
let aabb = HEART.aabb_2d(isometry);
gizmos.rect_2d(aabb.center(), aabb.half_size() * 2., WHITE);
}
BoundingShape::BoundingSphere => {
// Get the bounding sphere of the primitive with the rotation and translation of the mesh.
let bounding_circle = HEART.bounding_circle(isometry);
gizmos
.circle_2d(bounding_circle.center(), bounding_circle.radius(), WHITE)
.resolution(64);
}
}
}
}
// Rotate the 3D shapes.
fn rotate_3d_shapes(mut shapes: Query<&mut Transform, With<Shape3d>>, time: Res<Time>) {
let delta_seconds = time.delta_secs();
for mut transform in shapes.iter_mut() {
transform.rotate_y(delta_seconds);
}
}
// Draw the AABBs or bounding spheres for the 3D shapes.
fn bounding_shapes_3d(
shapes: Query<&Transform, With<Shape3d>>,
mut gizmos: Gizmos,
bounding_shape: Res<State<BoundingShape>>,
) {
for transform in shapes.iter() {
match bounding_shape.get() {
BoundingShape::None => (),
BoundingShape::BoundingBox => {
// Get the AABB of the extrusion with the rotation and translation of the mesh.
let aabb = EXTRUSION.aabb_3d(transform.to_isometry());
gizmos.primitive_3d(
&Cuboid::from_size(Vec3::from(aabb.half_size()) * 2.),
aabb.center(),
WHITE,
);
}
BoundingShape::BoundingSphere => {
// Get the bounding sphere of the extrusion with the rotation and translation of the mesh.
let bounding_sphere = EXTRUSION.bounding_sphere(transform.to_isometry());
gizmos.sphere(bounding_sphere.center(), bounding_sphere.radius(), WHITE);
}
}
}
}
// Switch to the next bounding shape.
fn update_bounding_shape(
current: Res<State<BoundingShape>>,
mut next: ResMut<NextState<BoundingShape>>,
) {
next.set(match current.get() {
BoundingShape::None => BoundingShape::BoundingBox,
BoundingShape::BoundingBox => BoundingShape::BoundingSphere,
BoundingShape::BoundingSphere => BoundingShape::None,
});
}
// Switch between 2D and 3D cameras.
fn switch_cameras(
current: Res<State<CameraActive>>,
mut next: ResMut<NextState<CameraActive>>,
camera: Single<(&mut Transform, &mut Projection)>,
) {
let next_state = match current.get() {
CameraActive::Dim2 => CameraActive::Dim3,
CameraActive::Dim3 => CameraActive::Dim2,
};
next.set(next_state);
let (mut transform, mut projection) = camera.into_inner();
match next_state {
CameraActive::Dim2 => {
*transform = TRANSFORM_2D;
*projection = PROJECTION_2D;
}
CameraActive::Dim3 => {
*transform = TRANSFORM_3D;
*projection = PROJECTION_3D;
}
};
}
/// A custom 2D heart primitive. The heart is made up of two circles centered at `Vec2::new(±radius, 0.)` each with the same `radius`.
///
/// The tip of the heart connects the two circles at a 45° angle from `Vec3::NEG_Y`.
#[derive(Copy, Clone)]
struct Heart {
/// The radius of each wing of the heart
radius: f32,
}
// The `Primitive2d` or `Primitive3d` trait is required by almost all other traits for primitives in bevy.
// Depending on your shape, you should implement either one of them.
impl Primitive2d for Heart {}
impl Heart {
const fn new(radius: f32) -> Self {
Self { radius }
}
}
// The `Measured2d` and `Measured3d` traits are used to compute the perimeter, the area or the volume of a primitive.
// If you implement `Measured2d` for a 2D primitive, `Measured3d` is automatically implemented for `Extrusion<T>`.
impl Measured2d for Heart {
fn perimeter(&self) -> f32 {
self.radius * (2.5 * PI + ops::powf(2f32, 1.5) + 2.0)
}
fn area(&self) -> f32 {
let circle_area = PI * self.radius * self.radius;
let triangle_area = self.radius * self.radius * (1.0 + 2f32.sqrt()) / 2.0;
let cutout = triangle_area - circle_area * 3.0 / 16.0;
2.0 * circle_area + 4.0 * cutout
}
}
// The `Bounded2d` or `Bounded3d` traits are used to compute the Axis Aligned Bounding Boxes or bounding circles / spheres for primitives.
impl Bounded2d for Heart {
fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
let isometry = isometry.into();
// The center of the circle at the center of the right wing of the heart
let circle_center = isometry.rotation * Vec2::new(self.radius, 0.0);
// The maximum X and Y positions of the two circles of the wings of the heart.
let max_circle = circle_center.abs() + Vec2::splat(self.radius);
// Since the two circles of the heart are mirrored around the origin, the minimum position is the negative of the maximum.
let min_circle = -max_circle;
// The position of the tip at the bottom of the heart
let tip_position = isometry.rotation * Vec2::new(0.0, -self.radius * (1. + SQRT_2));
Aabb2d {
min: isometry.translation + min_circle.min(tip_position),
max: isometry.translation + max_circle.max(tip_position),
}
}
fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {
let isometry = isometry.into();
// The bounding circle of the heart is not at its origin. This `offset` is the offset between the center of the bounding circle and its translation.
let offset = self.radius / ops::powf(2f32, 1.5);
// The center of the bounding circle
let center = isometry * Vec2::new(0.0, -offset);
// The radius of the bounding circle
let radius = self.radius * (1.0 + 2f32.sqrt()) - offset;
BoundingCircle::new(center, radius)
}
}
// You can implement the `BoundedExtrusion` trait to implement `Bounded3d for Extrusion<Heart>`. There is a default implementation for both AABBs and bounding spheres,
// but you may be able to find faster solutions for your specific primitives.
impl BoundedExtrusion for Heart {}
// You can use the `Meshable` trait to create a `MeshBuilder` for the primitive.
impl Meshable for Heart {
// The `MeshBuilder` can be used to create the actual mesh for that primitive.
type Output = HeartMeshBuilder;
fn mesh(&self) -> Self::Output {
Self::Output {
heart: *self,
resolution: 32,
}
}
}
// You can include any additional information needed for meshing the primitive in the `MeshBuilder`.
struct HeartMeshBuilder {
heart: Heart,
// The resolution determines the amount of vertices used for each wing of the heart
resolution: usize,
}
// This trait is needed so that the configuration methods of the builder of the primitive are also available for the builder for the extrusion.
// If you do not want to support these configuration options for extrusions you can just implement them for your 2D `MeshBuilder`.
trait HeartBuilder {
/// Set the resolution for each of the wings of the heart.
fn resolution(self, resolution: usize) -> Self;
}
impl HeartBuilder for HeartMeshBuilder {
fn resolution(mut self, resolution: usize) -> Self {
self.resolution = resolution;
self
}
}
impl HeartBuilder for ExtrusionBuilder<Heart> {
fn resolution(mut self, resolution: usize) -> Self {
self.base_builder.resolution = resolution;
self
}
}
impl MeshBuilder for HeartMeshBuilder {
// This is where you should build the actual mesh.
fn build(&self) -> Mesh {
let radius = self.heart.radius;
// The curved parts of each wing (half) of the heart have an angle of `PI * 1.25` or 225°
let wing_angle = PI * 1.25;
// We create buffers for the vertices, their normals and UVs, as well as the indices used to connect the vertices.
let mut vertices = Vec::with_capacity(2 * self.resolution);
let mut uvs = Vec::with_capacity(2 * self.resolution);
let mut indices = Vec::with_capacity(6 * self.resolution - 9);
// Since the heart is flat, we know all the normals are identical already.
let normals = vec![[0f32, 0f32, 1f32]; 2 * self.resolution];
// The point in the middle of the two curved parts of the heart
vertices.push([0.0; 3]);
uvs.push([0.5, 0.5]);
// The left wing of the heart, starting from the point in the middle.
for i in 1..self.resolution {
let angle = (i as f32 / self.resolution as f32) * wing_angle;
let (sin, cos) = ops::sin_cos(angle);
vertices.push([radius * (cos - 1.0), radius * sin, 0.0]);
uvs.push([0.5 - (cos - 1.0) / 4., 0.5 - sin / 2.]);
}
// The bottom tip of the heart
vertices.push([0.0, radius * (-1. - SQRT_2), 0.0]);
uvs.push([0.5, 1.]);
// The right wing of the heart, starting from the bottom most point and going towards the middle point.
for i in 0..self.resolution - 1 {
let angle = (i as f32 / self.resolution as f32) * wing_angle - PI / 4.;
let (sin, cos) = ops::sin_cos(angle);
vertices.push([radius * (cos + 1.0), radius * sin, 0.0]);
uvs.push([0.5 - (cos + 1.0) / 4., 0.5 - sin / 2.]);
}
// This is where we build all the triangles from the points created above.
// Each triangle has one corner on the middle point with the other two being adjacent points on the perimeter of the heart.
for i in 2..2 * self.resolution as u32 {
indices.extend_from_slice(&[i - 1, i, 0]);
}
// Here, the actual `Mesh` is created. We set the indices, vertices, normals and UVs created above and specify the topology of the mesh.
Mesh::new(
bevy::render::mesh::PrimitiveTopology::TriangleList,
RenderAssetUsages::default(),
)
.with_inserted_indices(bevy::render::mesh::Indices::U32(indices))
.with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, vertices)
.with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals)
.with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs)
}
}
// The `Extrudable` trait can be used to easily implement meshing for extrusions.
impl Extrudable for HeartMeshBuilder {
fn perimeter(&self) -> Vec<PerimeterSegment> {
let resolution = self.resolution as u32;
vec![
// The left wing of the heart
PerimeterSegment::Smooth {
// The normals of the first and last vertices of smooth segments have to be specified manually.
first_normal: Vec2::X,
last_normal: Vec2::new(-1.0, -1.0).normalize(),
// These indices are used to index into the `ATTRIBUTE_POSITION` vec of your 2D mesh.
indices: (0..resolution).collect(),
},
// The bottom tip of the heart
PerimeterSegment::Flat {
indices: vec![resolution - 1, resolution, resolution + 1],
},
// The right wing of the heart
PerimeterSegment::Smooth {
first_normal: Vec2::new(1.0, -1.0).normalize(),
last_normal: Vec2::NEG_X,
indices: (resolution + 1..2 * resolution).chain([0]).collect(),
},
]
}
}

View File

@@ -0,0 +1,243 @@
//! This example shows how to sample random points from primitive shapes.
use bevy::{
input::mouse::{AccumulatedMouseMotion, MouseButtonInput},
math::prelude::*,
prelude::*,
render::mesh::SphereKind,
};
use rand::{distributions::Distribution, SeedableRng};
use rand_chacha::ChaCha8Rng;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Startup, setup)
.add_systems(Update, (handle_mouse, handle_keypress))
.run();
}
/// Resource for the random sampling mode, telling whether to sample the interior or the boundary.
#[derive(Resource)]
enum Mode {
Interior,
Boundary,
}
/// Resource storing the shape being sampled.
#[derive(Resource)]
struct SampledShape(Cuboid);
/// 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(Handle<StandardMaterial>);
/// Marker component for sampled points.
#[derive(Component)]
struct SamplePoint;
/// The pressed state of the mouse, used for camera motion.
#[derive(Resource)]
struct MousePressed(bool);
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
// Use seeded rng and store it in a resource; this makes the random output reproducible.
let seeded_rng = ChaCha8Rng::seed_from_u64(19878367467712);
commands.insert_resource(RandomSource(seeded_rng));
// Make a plane for establishing space.
commands.spawn((
Mesh3d(meshes.add(Plane3d::default().mesh().size(12.0, 12.0))),
MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))),
Transform::from_xyz(0.0, -2.5, 0.0),
));
// Store the shape we sample from in a resource:
let shape = Cuboid::from_length(2.9);
commands.insert_resource(SampledShape(shape));
// The sampled shape shown transparently:
commands.spawn((
Mesh3d(meshes.add(shape)),
MeshMaterial3d(materials.add(StandardMaterial {
base_color: Color::srgba(0.2, 0.1, 0.6, 0.3),
alpha_mode: AlphaMode::Blend,
cull_mode: None,
..default()
})),
));
// A light:
commands.spawn((
PointLight {
shadows_enabled: true,
..default()
},
Transform::from_xyz(4.0, 8.0, 4.0),
));
// A camera:
commands.spawn((
Camera3d::default(),
Transform::from_xyz(-2.0, 3.0, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
));
// Store the mesh and material for sample points in resources:
commands.insert_resource(PointMesh(
meshes.add(
Sphere::new(0.03)
.mesh()
.kind(SphereKind::Ico { subdivisions: 3 }),
),
));
commands.insert_resource(PointMaterial(materials.add(StandardMaterial {
base_color: Color::srgb(1.0, 0.8, 0.8),
metallic: 0.8,
..default()
})));
// Instructions for the example:
commands.spawn((
Text::new(
"Controls:\n\
M: Toggle between sampling boundary and interior.\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 left/right.",
),
Node {
position_type: PositionType::Absolute,
top: Val::Px(12.0),
left: Val::Px(12.0),
..default()
},
));
// The mode starts with interior points.
commands.insert_resource(Mode::Interior);
// Starting mouse-pressed state is false.
commands.insert_resource(MousePressed(false));
}
// Handle user inputs from the keyboard:
fn handle_keypress(
mut commands: Commands,
keyboard: Res<ButtonInput<KeyCode>>,
mut mode: ResMut<Mode>,
shape: Res<SampledShape>,
mut random_source: ResMut<RandomSource>,
sample_mesh: Res<PointMesh>,
sample_material: Res<PointMaterial>,
samples: Query<Entity, With<SamplePoint>>,
) {
// R => restart, deleting all samples
if keyboard.just_pressed(KeyCode::KeyR) {
for entity in &samples {
commands.entity(entity).despawn();
}
}
// S => sample once
if keyboard.just_pressed(KeyCode::KeyS) {
let rng = &mut random_source.0;
// Get a single random Vec3:
let sample: Vec3 = match *mode {
Mode::Interior => shape.0.sample_interior(rng),
Mode::Boundary => shape.0.sample_boundary(rng),
};
// Spawn a sphere at the random location:
commands.spawn((
Mesh3d(sample_mesh.0.clone()),
MeshMaterial3d(sample_material.0.clone()),
Transform::from_translation(sample),
SamplePoint,
));
// NOTE: The point is inside the cube created at setup just because of how the
// scene is constructed; in general, you would want to use something like
// `cube_transform.transform_point(sample)` to get the position of where the sample
// would be after adjusting for the position and orientation of the cube.
//
// If the spawned point also needed to follow the position of the cube as it moved,
// then making it a child entity of the cube would be a good approach.
}
// D => generate many samples
if keyboard.just_pressed(KeyCode::KeyD) {
let mut rng = &mut random_source.0;
// Get 100 random Vec3s:
let samples: Vec<Vec3> = match *mode {
Mode::Interior => {
let dist = shape.0.interior_dist();
dist.sample_iter(&mut rng).take(100).collect()
}
Mode::Boundary => {
let dist = shape.0.boundary_dist();
dist.sample_iter(&mut rng).take(100).collect()
}
};
// For each sample point, spawn a sphere:
for sample in samples {
commands.spawn((
Mesh3d(sample_mesh.0.clone()),
MeshMaterial3d(sample_material.0.clone()),
Transform::from_translation(sample),
SamplePoint,
));
}
// NOTE: See the previous note above regarding the positioning of these samples
// relative to the transform of the cube containing them.
}
// M => toggle mode between interior and boundary.
if keyboard.just_pressed(KeyCode::KeyM) {
match *mode {
Mode::Interior => *mode = Mode::Boundary,
Mode::Boundary => *mode = Mode::Interior,
}
}
}
// Handle user mouse input for panning the camera around:
fn handle_mouse(
accumulated_mouse_motion: Res<AccumulatedMouseMotion>,
mut button_events: EventReader<MouseButtonInput>,
mut camera_transform: Single<&mut Transform, With<Camera>>,
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 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.x;
camera_transform.rotate_around(Vec3::ZERO, Quat::from_rotation_y(-displacement / 150.));
}
}

View File

@@ -0,0 +1,706 @@
//! This example demonstrates how each of Bevy's math primitives look like in 2D and 3D with meshes
//! and with gizmos
use bevy::{input::common_conditions::input_just_pressed, math::Isometry2d, prelude::*};
const LEFT_RIGHT_OFFSET_2D: f32 = 200.0;
const LEFT_RIGHT_OFFSET_3D: f32 = 2.0;
fn main() {
let mut app = App::new();
app.add_plugins(DefaultPlugins)
.init_state::<PrimitiveSelected>()
.init_state::<CameraActive>();
// cameras
app.add_systems(Startup, (setup_cameras, setup_lights, setup_ambient_light))
.add_systems(
Update,
(
update_active_cameras.run_if(state_changed::<CameraActive>),
switch_cameras.run_if(input_just_pressed(KeyCode::KeyC)),
),
);
// text
// PostStartup since we need the cameras to exist
app.add_systems(PostStartup, setup_text);
app.add_systems(
Update,
(update_text.run_if(state_changed::<PrimitiveSelected>),),
);
// primitives
app.add_systems(Startup, (spawn_primitive_2d, spawn_primitive_3d))
.add_systems(
Update,
(
switch_to_next_primitive.run_if(input_just_pressed(KeyCode::ArrowUp)),
switch_to_previous_primitive.run_if(input_just_pressed(KeyCode::ArrowDown)),
draw_gizmos_2d.run_if(in_mode(CameraActive::Dim2)),
draw_gizmos_3d.run_if(in_mode(CameraActive::Dim3)),
update_primitive_meshes
.run_if(state_changed::<PrimitiveSelected>.or(state_changed::<CameraActive>)),
rotate_primitive_2d_meshes,
rotate_primitive_3d_meshes,
),
);
app.run();
}
/// State for tracking which of the two cameras (2D & 3D) is currently active
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, States, Default, Reflect)]
enum CameraActive {
#[default]
/// 2D Camera is active
Dim2,
/// 3D Camera is active
Dim3,
}
/// State for tracking which primitives are currently displayed
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, States, Default, Reflect)]
enum PrimitiveSelected {
#[default]
RectangleAndCuboid,
CircleAndSphere,
Ellipse,
Triangle,
Plane,
Line,
Segment,
Polyline,
Polygon,
RegularPolygon,
Capsule,
Cylinder,
Cone,
ConicalFrustum,
Torus,
Tetrahedron,
Arc,
CircularSector,
CircularSegment,
}
impl std::fmt::Display for PrimitiveSelected {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let name = match self {
PrimitiveSelected::RectangleAndCuboid => String::from("Rectangle/Cuboid"),
PrimitiveSelected::CircleAndSphere => String::from("Circle/Sphere"),
other => format!("{other:?}"),
};
write!(f, "{name}")
}
}
impl PrimitiveSelected {
const ALL: [Self; 19] = [
Self::RectangleAndCuboid,
Self::CircleAndSphere,
Self::Ellipse,
Self::Triangle,
Self::Plane,
Self::Line,
Self::Segment,
Self::Polyline,
Self::Polygon,
Self::RegularPolygon,
Self::Capsule,
Self::Cylinder,
Self::Cone,
Self::ConicalFrustum,
Self::Torus,
Self::Tetrahedron,
Self::Arc,
Self::CircularSector,
Self::CircularSegment,
];
fn next(self) -> Self {
Self::ALL
.into_iter()
.cycle()
.skip_while(|&x| x != self)
.nth(1)
.unwrap()
}
fn previous(self) -> Self {
Self::ALL
.into_iter()
.rev()
.cycle()
.skip_while(|&x| x != self)
.nth(1)
.unwrap()
}
}
const SMALL_2D: f32 = 50.0;
const BIG_2D: f32 = 100.0;
const SMALL_3D: f32 = 0.5;
const BIG_3D: f32 = 1.0;
// primitives
const RECTANGLE: Rectangle = Rectangle {
half_size: Vec2::new(SMALL_2D, BIG_2D),
};
const CUBOID: Cuboid = Cuboid {
half_size: Vec3::new(BIG_3D, SMALL_3D, BIG_3D),
};
const CIRCLE: Circle = Circle { radius: BIG_2D };
const SPHERE: Sphere = Sphere { radius: BIG_3D };
const ELLIPSE: Ellipse = Ellipse {
half_size: Vec2::new(BIG_2D, SMALL_2D),
};
const TRIANGLE_2D: Triangle2d = Triangle2d {
vertices: [
Vec2::new(BIG_2D, 0.0),
Vec2::new(0.0, BIG_2D),
Vec2::new(-BIG_2D, 0.0),
],
};
const TRIANGLE_3D: Triangle3d = Triangle3d {
vertices: [
Vec3::new(BIG_3D, 0.0, 0.0),
Vec3::new(0.0, BIG_3D, 0.0),
Vec3::new(-BIG_3D, 0.0, 0.0),
],
};
const PLANE_2D: Plane2d = Plane2d { normal: Dir2::Y };
const PLANE_3D: Plane3d = Plane3d {
normal: Dir3::Y,
half_size: Vec2::new(BIG_3D, BIG_3D),
};
const LINE2D: Line2d = Line2d { direction: Dir2::X };
const LINE3D: Line3d = Line3d { direction: Dir3::X };
const SEGMENT_2D: Segment2d = Segment2d {
vertices: [Vec2::new(-BIG_2D / 2., 0.), Vec2::new(BIG_2D / 2., 0.)],
};
const SEGMENT_3D: Segment3d = Segment3d {
vertices: [
Vec3::new(-BIG_3D / 2., 0., 0.),
Vec3::new(BIG_3D / 2., 0., 0.),
],
};
const POLYLINE_2D: Polyline2d<4> = Polyline2d {
vertices: [
Vec2::new(-BIG_2D, -SMALL_2D),
Vec2::new(-SMALL_2D, SMALL_2D),
Vec2::new(SMALL_2D, -SMALL_2D),
Vec2::new(BIG_2D, SMALL_2D),
],
};
const POLYLINE_3D: Polyline3d<4> = Polyline3d {
vertices: [
Vec3::new(-BIG_3D, -SMALL_3D, -SMALL_3D),
Vec3::new(SMALL_3D, SMALL_3D, 0.0),
Vec3::new(-SMALL_3D, -SMALL_3D, 0.0),
Vec3::new(BIG_3D, SMALL_3D, SMALL_3D),
],
};
const POLYGON_2D: Polygon<5> = Polygon {
vertices: [
Vec2::new(-BIG_2D, -SMALL_2D),
Vec2::new(BIG_2D, -SMALL_2D),
Vec2::new(BIG_2D, SMALL_2D),
Vec2::new(0.0, 0.0),
Vec2::new(-BIG_2D, SMALL_2D),
],
};
const REGULAR_POLYGON: RegularPolygon = RegularPolygon {
circumcircle: Circle { radius: BIG_2D },
sides: 5,
};
const CAPSULE_2D: Capsule2d = Capsule2d {
radius: SMALL_2D,
half_length: SMALL_2D,
};
const CAPSULE_3D: Capsule3d = Capsule3d {
radius: SMALL_3D,
half_length: SMALL_3D,
};
const CYLINDER: Cylinder = Cylinder {
radius: SMALL_3D,
half_height: SMALL_3D,
};
const CONE: Cone = Cone {
radius: BIG_3D,
height: BIG_3D,
};
const CONICAL_FRUSTUM: ConicalFrustum = ConicalFrustum {
radius_top: BIG_3D,
radius_bottom: SMALL_3D,
height: BIG_3D,
};
const ANNULUS: Annulus = Annulus {
inner_circle: Circle { radius: SMALL_2D },
outer_circle: Circle { radius: BIG_2D },
};
const TORUS: Torus = Torus {
minor_radius: SMALL_3D / 2.0,
major_radius: SMALL_3D * 1.5,
};
const TETRAHEDRON: Tetrahedron = Tetrahedron {
vertices: [
Vec3::new(-BIG_3D, 0.0, 0.0),
Vec3::new(BIG_3D, 0.0, 0.0),
Vec3::new(0.0, 0.0, -BIG_3D * 1.67),
Vec3::new(0.0, BIG_3D * 1.67, -BIG_3D * 0.5),
],
};
const ARC: Arc2d = Arc2d {
radius: BIG_2D,
half_angle: std::f32::consts::FRAC_PI_4,
};
const CIRCULAR_SECTOR: CircularSector = CircularSector {
arc: Arc2d {
radius: BIG_2D,
half_angle: std::f32::consts::FRAC_PI_4,
},
};
const CIRCULAR_SEGMENT: CircularSegment = CircularSegment {
arc: Arc2d {
radius: BIG_2D,
half_angle: std::f32::consts::FRAC_PI_4,
},
};
fn setup_cameras(mut commands: Commands) {
let start_in_2d = true;
let make_camera = |is_active| Camera {
is_active,
..Default::default()
};
commands.spawn((Camera2d, make_camera(start_in_2d)));
commands.spawn((
Camera3d::default(),
make_camera(!start_in_2d),
Transform::from_xyz(0.0, 10.0, 0.0).looking_at(Vec3::ZERO, Vec3::Z),
));
}
fn setup_ambient_light(mut ambient_light: ResMut<AmbientLight>) {
ambient_light.brightness = 50.0;
}
fn setup_lights(mut commands: Commands) {
commands.spawn((
PointLight {
intensity: 5000.0,
..default()
},
Transform::from_translation(Vec3::new(-LEFT_RIGHT_OFFSET_3D, 2.0, 0.0))
.looking_at(Vec3::new(-LEFT_RIGHT_OFFSET_3D, 0.0, 0.0), Vec3::Y),
));
}
/// Marker component for header text
#[derive(Debug, Clone, Component, Default, Reflect)]
pub struct HeaderText;
/// Marker component for header node
#[derive(Debug, Clone, Component, Default, Reflect)]
pub struct HeaderNode;
fn update_active_cameras(
state: Res<State<CameraActive>>,
camera_2d: Single<(Entity, &mut Camera), With<Camera2d>>,
camera_3d: Single<(Entity, &mut Camera), (With<Camera3d>, Without<Camera2d>)>,
mut text: Query<&mut UiTargetCamera, With<HeaderNode>>,
) {
let (entity_2d, mut cam_2d) = camera_2d.into_inner();
let (entity_3d, mut cam_3d) = camera_3d.into_inner();
let is_camera_2d_active = matches!(*state.get(), CameraActive::Dim2);
cam_2d.is_active = is_camera_2d_active;
cam_3d.is_active = !is_camera_2d_active;
let active_camera = if is_camera_2d_active {
entity_2d
} else {
entity_3d
};
text.iter_mut().for_each(|mut target_camera| {
*target_camera = UiTargetCamera(active_camera);
});
}
fn switch_cameras(current: Res<State<CameraActive>>, mut next: ResMut<NextState<CameraActive>>) {
let next_state = match current.get() {
CameraActive::Dim2 => CameraActive::Dim3,
CameraActive::Dim3 => CameraActive::Dim2,
};
next.set(next_state);
}
fn setup_text(mut commands: Commands, cameras: Query<(Entity, &Camera)>) {
let active_camera = cameras
.iter()
.find_map(|(entity, camera)| camera.is_active.then_some(entity))
.expect("run condition ensures existence");
commands.spawn((
HeaderNode,
Node {
justify_self: JustifySelf::Center,
top: Val::Px(5.0),
..Default::default()
},
UiTargetCamera(active_camera),
children![(
Text::default(),
HeaderText,
TextLayout::new_with_justify(JustifyText::Center),
children![
TextSpan::new("Primitive: "),
TextSpan(format!("{text}", text = PrimitiveSelected::default())),
TextSpan::new("\n\n"),
TextSpan::new(
"Press 'C' to switch between 2D and 3D mode\n\
Press 'Up' or 'Down' to switch to the next/previous primitive",
),
TextSpan::new("\n\n"),
TextSpan::new("(If nothing is displayed, there's no rendering support yet)",),
]
)],
));
}
fn update_text(
primitive_state: Res<State<PrimitiveSelected>>,
header: Query<Entity, With<HeaderText>>,
mut writer: TextUiWriter,
) {
let new_text = format!("{text}", text = primitive_state.get());
header.iter().for_each(|header_text| {
if let Some(mut text) = writer.get_text(header_text, 2) {
(*text).clone_from(&new_text);
};
});
}
fn switch_to_next_primitive(
current: Res<State<PrimitiveSelected>>,
mut next: ResMut<NextState<PrimitiveSelected>>,
) {
let next_state = current.get().next();
next.set(next_state);
}
fn switch_to_previous_primitive(
current: Res<State<PrimitiveSelected>>,
mut next: ResMut<NextState<PrimitiveSelected>>,
) {
let next_state = current.get().previous();
next.set(next_state);
}
fn in_mode(active: CameraActive) -> impl Fn(Res<State<CameraActive>>) -> bool {
move |state| *state.get() == active
}
fn draw_gizmos_2d(mut gizmos: Gizmos, state: Res<State<PrimitiveSelected>>, time: Res<Time>) {
const POSITION: Vec2 = Vec2::new(-LEFT_RIGHT_OFFSET_2D, 0.0);
let angle = time.elapsed_secs();
let isometry = Isometry2d::new(POSITION, Rot2::radians(angle));
let color = Color::WHITE;
#[expect(
clippy::match_same_arms,
reason = "Certain primitives don't have any 2D rendering support yet."
)]
match state.get() {
PrimitiveSelected::RectangleAndCuboid => {
gizmos.primitive_2d(&RECTANGLE, isometry, color);
}
PrimitiveSelected::CircleAndSphere => {
gizmos.primitive_2d(&CIRCLE, isometry, color);
}
PrimitiveSelected::Ellipse => drop(gizmos.primitive_2d(&ELLIPSE, isometry, color)),
PrimitiveSelected::Triangle => gizmos.primitive_2d(&TRIANGLE_2D, isometry, color),
PrimitiveSelected::Plane => gizmos.primitive_2d(&PLANE_2D, isometry, color),
PrimitiveSelected::Line => drop(gizmos.primitive_2d(&LINE2D, isometry, color)),
PrimitiveSelected::Segment => {
drop(gizmos.primitive_2d(&SEGMENT_2D, isometry, color));
}
PrimitiveSelected::Polyline => gizmos.primitive_2d(&POLYLINE_2D, isometry, color),
PrimitiveSelected::Polygon => gizmos.primitive_2d(&POLYGON_2D, isometry, color),
PrimitiveSelected::RegularPolygon => {
gizmos.primitive_2d(&REGULAR_POLYGON, isometry, color);
}
PrimitiveSelected::Capsule => gizmos.primitive_2d(&CAPSULE_2D, isometry, color),
PrimitiveSelected::Cylinder => {}
PrimitiveSelected::Cone => {}
PrimitiveSelected::ConicalFrustum => {}
PrimitiveSelected::Torus => drop(gizmos.primitive_2d(&ANNULUS, isometry, color)),
PrimitiveSelected::Tetrahedron => {}
PrimitiveSelected::Arc => gizmos.primitive_2d(&ARC, isometry, color),
PrimitiveSelected::CircularSector => {
gizmos.primitive_2d(&CIRCULAR_SECTOR, isometry, color);
}
PrimitiveSelected::CircularSegment => {
gizmos.primitive_2d(&CIRCULAR_SEGMENT, isometry, color);
}
}
}
/// Marker for primitive meshes to record in which state they should be visible in
#[derive(Debug, Clone, Component, Default, Reflect)]
pub struct PrimitiveData {
camera_mode: CameraActive,
primitive_state: PrimitiveSelected,
}
/// Marker for meshes of 2D primitives
#[derive(Debug, Clone, Component, Default)]
pub struct MeshDim2;
/// Marker for meshes of 3D primitives
#[derive(Debug, Clone, Component, Default)]
pub struct MeshDim3;
fn spawn_primitive_2d(
mut commands: Commands,
mut materials: ResMut<Assets<ColorMaterial>>,
mut meshes: ResMut<Assets<Mesh>>,
) {
const POSITION: Vec3 = Vec3::new(LEFT_RIGHT_OFFSET_2D, 0.0, 0.0);
let material: Handle<ColorMaterial> = materials.add(Color::WHITE);
let camera_mode = CameraActive::Dim2;
[
Some(RECTANGLE.mesh().build()),
Some(CIRCLE.mesh().build()),
Some(ELLIPSE.mesh().build()),
Some(TRIANGLE_2D.mesh().build()),
None, // plane
None, // line
None, // segment
None, // polyline
None, // polygon
Some(REGULAR_POLYGON.mesh().build()),
Some(CAPSULE_2D.mesh().build()),
None, // cylinder
None, // cone
None, // conical frustum
Some(ANNULUS.mesh().build()),
None, // tetrahedron
]
.into_iter()
.zip(PrimitiveSelected::ALL)
.for_each(|(maybe_mesh, state)| {
if let Some(mesh) = maybe_mesh {
commands.spawn((
MeshDim2,
PrimitiveData {
camera_mode,
primitive_state: state,
},
Mesh2d(meshes.add(mesh)),
MeshMaterial2d(material.clone()),
Transform::from_translation(POSITION),
));
}
});
}
fn spawn_primitive_3d(
mut commands: Commands,
mut materials: ResMut<Assets<StandardMaterial>>,
mut meshes: ResMut<Assets<Mesh>>,
) {
const POSITION: Vec3 = Vec3::new(-LEFT_RIGHT_OFFSET_3D, 0.0, 0.0);
let material: Handle<StandardMaterial> = materials.add(Color::WHITE);
let camera_mode = CameraActive::Dim3;
[
Some(CUBOID.mesh().build()),
Some(SPHERE.mesh().build()),
None, // ellipse
Some(TRIANGLE_3D.mesh().build()),
Some(PLANE_3D.mesh().build()),
None, // line
None, // segment
None, // polyline
None, // polygon
None, // regular polygon
Some(CAPSULE_3D.mesh().build()),
Some(CYLINDER.mesh().build()),
None, // cone
None, // conical frustum
Some(TORUS.mesh().build()),
Some(TETRAHEDRON.mesh().build()),
]
.into_iter()
.zip(PrimitiveSelected::ALL)
.for_each(|(maybe_mesh, state)| {
if let Some(mesh) = maybe_mesh {
commands.spawn((
MeshDim3,
PrimitiveData {
camera_mode,
primitive_state: state,
},
Mesh3d(meshes.add(mesh)),
MeshMaterial3d(material.clone()),
Transform::from_translation(POSITION),
));
}
});
}
fn update_primitive_meshes(
camera_state: Res<State<CameraActive>>,
primitive_state: Res<State<PrimitiveSelected>>,
mut primitives: Query<(&mut Visibility, &PrimitiveData)>,
) {
primitives.iter_mut().for_each(|(mut vis, primitive)| {
let visible = primitive.camera_mode == *camera_state.get()
&& primitive.primitive_state == *primitive_state.get();
*vis = if visible {
Visibility::Inherited
} else {
Visibility::Hidden
};
});
}
fn rotate_primitive_2d_meshes(
mut primitives_2d: Query<
(&mut Transform, &ViewVisibility),
(With<PrimitiveData>, With<MeshDim2>),
>,
time: Res<Time>,
) {
let rotation_2d = Quat::from_mat3(&Mat3::from_angle(time.elapsed_secs()));
primitives_2d
.iter_mut()
.filter(|(_, vis)| vis.get())
.for_each(|(mut transform, _)| {
transform.rotation = rotation_2d;
});
}
fn rotate_primitive_3d_meshes(
mut primitives_3d: Query<
(&mut Transform, &ViewVisibility),
(With<PrimitiveData>, With<MeshDim3>),
>,
time: Res<Time>,
) {
let rotation_3d = Quat::from_rotation_arc(
Vec3::Z,
Vec3::new(
ops::sin(time.elapsed_secs()),
ops::cos(time.elapsed_secs()),
ops::sin(time.elapsed_secs()) * 0.5,
)
.try_normalize()
.unwrap_or(Vec3::Z),
);
primitives_3d
.iter_mut()
.filter(|(_, vis)| vis.get())
.for_each(|(mut transform, _)| {
transform.rotation = rotation_3d;
});
}
fn draw_gizmos_3d(mut gizmos: Gizmos, state: Res<State<PrimitiveSelected>>, time: Res<Time>) {
const POSITION: Vec3 = Vec3::new(LEFT_RIGHT_OFFSET_3D, 0.0, 0.0);
let rotation = Quat::from_rotation_arc(
Vec3::Z,
Vec3::new(
ops::sin(time.elapsed_secs()),
ops::cos(time.elapsed_secs()),
ops::sin(time.elapsed_secs()) * 0.5,
)
.try_normalize()
.unwrap_or(Vec3::Z),
);
let isometry = Isometry3d::new(POSITION, rotation);
let color = Color::WHITE;
let resolution = 10;
#[expect(
clippy::match_same_arms,
reason = "Certain primitives don't have any 3D rendering support yet."
)]
match state.get() {
PrimitiveSelected::RectangleAndCuboid => {
gizmos.primitive_3d(&CUBOID, isometry, color);
}
PrimitiveSelected::CircleAndSphere => drop(
gizmos
.primitive_3d(&SPHERE, isometry, color)
.resolution(resolution),
),
PrimitiveSelected::Ellipse => {}
PrimitiveSelected::Triangle => gizmos.primitive_3d(&TRIANGLE_3D, isometry, color),
PrimitiveSelected::Plane => drop(gizmos.primitive_3d(&PLANE_3D, isometry, color)),
PrimitiveSelected::Line => gizmos.primitive_3d(&LINE3D, isometry, color),
PrimitiveSelected::Segment => gizmos.primitive_3d(&SEGMENT_3D, isometry, color),
PrimitiveSelected::Polyline => gizmos.primitive_3d(&POLYLINE_3D, isometry, color),
PrimitiveSelected::Polygon => {}
PrimitiveSelected::RegularPolygon => {}
PrimitiveSelected::Capsule => drop(
gizmos
.primitive_3d(&CAPSULE_3D, isometry, color)
.resolution(resolution),
),
PrimitiveSelected::Cylinder => drop(
gizmos
.primitive_3d(&CYLINDER, isometry, color)
.resolution(resolution),
),
PrimitiveSelected::Cone => drop(
gizmos
.primitive_3d(&CONE, isometry, color)
.resolution(resolution),
),
PrimitiveSelected::ConicalFrustum => {
gizmos.primitive_3d(&CONICAL_FRUSTUM, isometry, color);
}
PrimitiveSelected::Torus => drop(
gizmos
.primitive_3d(&TORUS, isometry, color)
.minor_resolution(resolution)
.major_resolution(resolution),
),
PrimitiveSelected::Tetrahedron => {
gizmos.primitive_3d(&TETRAHEDRON, isometry, color);
}
PrimitiveSelected::Arc => {}
PrimitiveSelected::CircularSector => {}
PrimitiveSelected::CircularSegment => {}
}
}

View File

@@ -0,0 +1,686 @@
//! 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",
),
Node {
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:
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 behavior at the poles of the rotation
camera_rig.pitch = camera_rig.pitch.clamp(-PI / 2.01, PI / 2.01);
}
}
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_secs();
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_secs();
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);
}
}