diff --git a/Cargo.toml b/Cargo.toml index 0a0ea25aab5a3..634ef2391dd88 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3244,6 +3244,18 @@ description = "A first-person camera that uses a world model and a view model wi category = "Camera" wasm = true +[[example]] +name = "projection_zoom" +path = "examples/camera/projection_zoom.rs" +doc-scrape-examples = true + +[package.metadata.example.projection_zoom] +name = "Projection Zoom" +description = "Shows how to zoom and orbit orthographic and perspective projection cameras." +category = "Camera" +wasm = true + + [package.metadata.example.fps_overlay] name = "FPS overlay" description = "Demonstrates FPS overlay" diff --git a/examples/README.md b/examples/README.md index e2e3bbf426d40..542a82764048d 100644 --- a/examples/README.md +++ b/examples/README.md @@ -255,6 +255,7 @@ Example | Description --- | --- [2D top-down camera](../examples/camera/2d_top_down_camera.rs) | A 2D top-down camera smoothly following player movements [First person view model](../examples/camera/first_person_view_model.rs) | A first-person camera that uses a world model and a view model with different field of views (FOV) +[Projection Zoom](../examples/camera/projection_zoom.rs) | Shows how to zoom and orbit orthographic and perspective projection cameras. ## Dev tools diff --git a/examples/camera/projection_zoom.rs b/examples/camera/projection_zoom.rs new file mode 100644 index 0000000000000..d490d971b8e98 --- /dev/null +++ b/examples/camera/projection_zoom.rs @@ -0,0 +1,245 @@ +//! Shows how to zoom and orbit orthographic and perspective projection cameras. + +use std::{ + f32::consts::{FRAC_PI_2, PI}, + ops::Range, +}; + +use bevy::{input::mouse::AccumulatedMouseScroll, prelude::*, render::camera::ScalingMode}; + +#[derive(Debug, Default, Resource)] +struct CameraSettings { + pub orbit_distance: f32, + // Multiply keyboard inputs by this factor + pub orbit_speed: f32, + // Clamp fixed vertical scale to this range + pub orthographic_zoom_range: Range, + // Multiply mouse wheel inputs by this factor + pub orthographic_zoom_speed: f32, + // Clamp field of view to this range + pub perspective_zoom_range: Range, + // Multiply mouse wheel inputs by this factor + pub perspective_zoom_speed: f32, + // Clamp pitch to this range + pub pitch_range: Range, +} + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .init_resource::() + .add_systems(Startup, (setup, instructions)) + .add_systems(Update, (orbit, switch_projection, zoom)) + .run(); +} + +/// Set up a simple 3D scene +fn setup( + mut camera_settings: ResMut, + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + // Perspective projections use field of view, expressed in radians. We would + // normally not set it to more than π, which represents a 180° FOV. + let min_fov = PI / 5.; + let max_fov = PI - 0.2; + + // In orthographic projections, we specify sizes in world units. The below values + // are very roughly similar to the above FOV settings, in terms of how "far away" + // the subject will appear when used with FixedVertical scaling mode. + let min_zoom = 5.0; + let max_zoom = 150.0; + + // Limiting pitch stops some unexpected rotation past 90° up or down. + let pitch_limit = FRAC_PI_2 - 0.01; + + camera_settings.orbit_distance = 10.0; + camera_settings.orbit_speed = 1.0; + camera_settings.orthographic_zoom_range = min_zoom..max_zoom; + camera_settings.orthographic_zoom_speed = 1.0; + camera_settings.perspective_zoom_range = min_fov..max_fov; + // Changes in FOV are much more noticeable due to its limited range in radians + camera_settings.perspective_zoom_speed = 0.05; + camera_settings.pitch_range = -pitch_limit..pitch_limit; + + commands.spawn(( + Name::new("Camera"), + Camera3dBundle { + projection: OrthographicProjection { + scaling_mode: ScalingMode::FixedVertical( + camera_settings.orthographic_zoom_range.start, + ), + ..OrthographicProjection::default_3d() + } + .into(), + transform: Transform::from_xyz(5.0, 5.0, 5.0).looking_at(Vec3::ZERO, Vec3::Y), + ..default() + }, + )); + + commands.spawn(( + Name::new("Plane"), + PbrBundle { + mesh: meshes.add(Plane3d::default().mesh().size(5.0, 5.0)), + material: materials.add(StandardMaterial { + base_color: Color::srgb(0.3, 0.5, 0.3), + // Turning off culling keeps the plane visible when viewed from beneath. + cull_mode: None, + ..default() + }), + ..default() + }, + )); + + commands.spawn(( + Name::new("Cube"), + PbrBundle { + mesh: meshes.add(Cuboid::default()), + material: materials.add(Color::srgb(0.8, 0.7, 0.6)), + transform: Transform::from_xyz(1.5, 0.51, 1.5), + ..default() + }, + )); + + commands.spawn(( + Name::new("Light"), + PointLightBundle { + transform: Transform::from_xyz(3.0, 8.0, 5.0), + ..default() + }, + )); +} + +fn instructions(mut commands: Commands) { + commands + .spawn(( + Name::new("Instructions"), + NodeBundle { + style: Style { + align_items: AlignItems::Start, + flex_direction: FlexDirection::Column, + justify_content: JustifyContent::Start, + width: Val::Percent(100.), + ..default() + }, + ..default() + }, + )) + .with_children(|parent| { + parent.spawn(TextBundle::from_section( + "Scroll mouse wheel to zoom in/out", + TextStyle::default(), + )); + parent.spawn(TextBundle::from_section( + "W or S: pitch", + TextStyle::default(), + )); + parent.spawn(TextBundle::from_section( + "A or D: yaw", + TextStyle::default(), + )); + }); +} + +fn orbit( + mut camera: Query<&mut Transform, With>, + camera_settings: Res, + keyboard_input: Res>, + time: Res