//! Demonstrates rigid as well as loose constellation clamp on the ground plane. //! //! 1. There is a main viewport (maximap) and a minimap at the right bottom. //! 2. Both have their own trackball controller. //! 3. The minimap is additionally sensitive to the controller of the maximap. //! 5. When orbiting the maximap down by pressing `k`, it will stop as soon as the initially lower //! positioned minimap camera hits the ground plane. //! 6. Toggling from rigid to loose constellation clamp by pressing `q` while keeping `k` pressed //! allows the maximap camera to orbit further until it hits the ground by itself. //! 7. Press `Return` to reset camera positions and try again. #![allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)] use std::f32::consts::PI; #[cfg(not(target_arch = "wasm32"))] use bevy::pbr::wireframe::{WireframeConfig, WireframePlugin}; use bevy::{ color::palettes::basic::SILVER, prelude::*, render::{ camera::Viewport, render_asset::RenderAssetUsages, render_resource::{Extent3d, TextureDimension, TextureFormat}, }, window::WindowResized, }; use bevy_trackball::prelude::*; fn main() { App::new() .add_plugins(( DefaultPlugins .set(ImagePlugin::default_nearest()) .set(WindowPlugin { primary_window: Some(Window { canvas: Some("#bevy".to_owned()), ..default() }), ..default() }), #[cfg(not(target_arch = "wasm32"))] WireframePlugin, )) .add_plugins(TrackballPlugin) .add_systems(Startup, setup) .add_systems( Update, ( rotate, #[cfg(not(target_arch = "wasm32"))] toggle_wireframe, ), ) .add_systems(Update, (resize_minimap, toggle_rigid_loose)) .run(); } /// A marker component for our shapes so we can query them separately from the ground plane #[derive(Component)] struct Shape; const SHAPES_X_EXTENT: f32 = 14.0; const EXTRUSION_X_EXTENT: f32 = 16.0; const Z_EXTENT: f32 = 5.0; fn setup( windows: Query<&Window>, mut commands: Commands, mut meshes: ResMut>, mut images: ResMut>, mut materials: ResMut>, ) { let debug_material = materials.add(StandardMaterial { base_color_texture: Some(images.add(uv_debug_texture())), ..default() }); let shapes = [ meshes.add(Cuboid::default()), meshes.add(Tetrahedron::default()), meshes.add(Capsule3d::default()), meshes.add(Torus::default()), meshes.add(Cylinder::default()), meshes.add(Cone::default()), meshes.add(ConicalFrustum::default()), meshes.add(Sphere::default().mesh().ico(5).unwrap()), meshes.add(Sphere::default().mesh().uv(32, 18)), ]; let extrusions = [ meshes.add(Extrusion::new(Rectangle::default(), 1.)), meshes.add(Extrusion::new(Capsule2d::default(), 1.)), meshes.add(Extrusion::new(Annulus::default(), 1.)), meshes.add(Extrusion::new(Circle::default(), 1.)), meshes.add(Extrusion::new(Ellipse::default(), 1.)), meshes.add(Extrusion::new(RegularPolygon::default(), 1.)), meshes.add(Extrusion::new(Triangle2d::default(), 1.)), ]; let num_shapes = shapes.len(); for (i, shape) in shapes.into_iter().enumerate() { commands.spawn(( PbrBundle { mesh: shape, material: debug_material.clone(), transform: Transform::from_xyz( -SHAPES_X_EXTENT / 2. + i as f32 / (num_shapes - 1) as f32 * SHAPES_X_EXTENT, 2.0, Z_EXTENT / 2.0, ) .with_rotation(Quat::from_rotation_x(-PI / 4.)), ..default() }, Shape, )); } let num_extrusions = extrusions.len(); for (i, shape) in extrusions.into_iter().enumerate() { commands.spawn(( PbrBundle { mesh: shape, material: debug_material.clone(), transform: Transform::from_xyz( -EXTRUSION_X_EXTENT / 2. + i as f32 / (num_extrusions - 1) as f32 * EXTRUSION_X_EXTENT, 2.0, -Z_EXTENT / 2., ) .with_rotation(Quat::from_rotation_x(-PI / 4.)), ..default() }, Shape, )); } // light commands.spawn(PointLightBundle { point_light: PointLight { shadows_enabled: true, intensity: 10_000_000., range: 100.0, shadow_depth_bias: 0.2, ..default() }, transform: Transform::from_xyz(8.0, 16.0, 8.0), ..default() }); // ground plane commands.spawn(PbrBundle { mesh: meshes.add(Plane3d::default().mesh().size(50.0, 50.0)), material: materials.add(Color::from(SILVER)), ..default() }); // cameras and boundary conditions let mut bound = Bound::default(); bound.min_target[1] = 0.0; bound.min_eye[1] = 0.3; bound.min_distance = 6.0; bound.max_distance = 50.0; let [target, eye, up] = [Vec3::Y, Vec3::new(0.0, 7.0, 14.0), Vec3::Y]; let maximap = commands .spawn(( TrackballController::default(), TrackballCamera::look_at(target, eye, up).with_clamp(bound.clone()), Camera3dBundle::default(), )) .id(); let window = windows.single(); let width = window.resolution.physical_width() / 3; let height = window.resolution.physical_height() / 3; let down = Quat::from_rotation_x(15f32.to_radians()); commands.spawn(( TrackballController::default(), TrackballCamera::look_at(target, down * (eye - target) + target, up) .with_clamp(bound) .add_controller(maximap, true), Camera3dBundle { camera: Camera { order: 1, clear_color: ClearColorConfig::None, viewport: Some(Viewport { physical_position: UVec2::new( window.resolution.physical_width() - width, window.resolution.physical_height() - height, ), physical_size: UVec2::new(width, height), ..default() }), ..default() }, ..default() }, MinimapCamera, )); // UI #[cfg(not(target_arch = "wasm32"))] commands.spawn(( TargetCamera(maximap), TextBundle::from_section("Press space to toggle wireframes", TextStyle::default()) .with_style(Style { position_type: PositionType::Absolute, top: Val::Px(12.0), left: Val::Px(12.0), ..default() }), )); commands.spawn(( Clamp, TargetCamera(maximap), TextBundle::from_section( "Rigid Constellation Clamp (Toggle: Q)", TextStyle::default(), ) .with_style(Style { position_type: PositionType::Absolute, bottom: Val::Px(10.0), left: Val::Px(10.0), ..default() }), )); } #[derive(Component)] struct Clamp; #[derive(Component)] struct MinimapCamera; #[allow(clippy::needless_pass_by_value)] fn resize_minimap( windows: Query<&Window>, mut resize_events: EventReader, mut minimap: Query<&mut Camera, With>, ) { for resize_event in resize_events.read() { let window = windows.get(resize_event.window).unwrap(); let mut minimap = minimap.single_mut(); let width = window.resolution.physical_width() / 3; let height = window.resolution.physical_height() / 3; minimap.viewport = Some(Viewport { physical_position: UVec2::new( window.resolution.physical_width() - width, window.resolution.physical_height() - height, ), physical_size: UVec2::new(width, height), ..default() }); } } #[allow(clippy::needless_pass_by_value)] fn toggle_rigid_loose( mut minimap: Query<&mut TrackballCamera, With>, mut text: Query<&mut Text, With>, keycode: Res>, ) { if keycode.just_pressed(KeyCode::KeyQ) { let mut text = text.single_mut(); let text = &mut text.sections[0].value; let mut minimap = minimap.single_mut(); let rigid = minimap.group.values_mut().next().unwrap(); if *rigid { *text = "Loose Constellation Clamp (Toggle: Q)".to_owned(); } else { *text = "Rigid Constellation Clamp (Toggle: Q)".to_owned(); } *rigid = !*rigid; } } fn rotate(mut query: Query<&mut Transform, With>, time: Res