Skip to main content

projection_zoom/
projection_zoom.rs

1//! Shows how to zoom orthographic and perspective projection cameras.
2
3use std::{f32::consts::PI, ops::Range};
4
5use bevy::{
6    camera::ScalingMode,
7    input::mouse::{AccumulatedMouseScroll, MouseScrollPixelsPerLine},
8    prelude::*,
9};
10
11#[derive(Debug, Resource)]
12struct CameraSettings {
13    /// The height of the viewport in world units when the orthographic camera's scale is 1
14    pub orthographic_viewport_height: f32,
15    /// Clamp the orthographic camera's scale to this range
16    pub orthographic_zoom_range: Range<f32>,
17    /// Multiply mouse wheel inputs by this factor when using the orthographic camera
18    pub orthographic_zoom_speed: f32,
19    /// Clamp perspective camera's field of view to this range
20    pub perspective_zoom_range: Range<f32>,
21    /// Multiply mouse wheel inputs by this factor when using the perspective camera
22    pub perspective_zoom_speed: f32,
23}
24
25fn main() {
26    App::new()
27        .add_plugins(DefaultPlugins)
28        .insert_resource(CameraSettings {
29            orthographic_viewport_height: 5.,
30            // In orthographic projections, we specify camera scale relative to a default value of 1,
31            // in which one unit in world space corresponds to one pixel.
32            orthographic_zoom_range: 0.1..10.0,
33            // This value was hand-tuned to ensure that zooming in and out feels smooth but not slow.
34            orthographic_zoom_speed: 0.2,
35            // Perspective projections use field of view, expressed in radians. We would
36            // normally not set it to more than π, which represents a 180° FOV.
37            perspective_zoom_range: (PI / 5.)..(PI - 0.2),
38            // Changes in FOV are much more noticeable due to its limited range in radians
39            perspective_zoom_speed: 0.05,
40        })
41        .add_systems(Startup, (setup, instructions))
42        .add_systems(Update, (switch_projection, zoom))
43        .run();
44}
45
46/// Set up a simple 3D scene
47fn setup(
48    asset_server: Res<AssetServer>,
49    camera_settings: Res<CameraSettings>,
50    mut commands: Commands,
51    mut meshes: ResMut<Assets<Mesh>>,
52    mut materials: ResMut<Assets<StandardMaterial>>,
53) {
54    commands.spawn((
55        Name::new("Camera"),
56        Camera3d::default(),
57        Projection::from(OrthographicProjection {
58            // We can set the scaling mode to FixedVertical to keep the viewport height constant as its aspect ratio changes.
59            // The viewport height is the height of the camera's view in world units when the scale is 1.
60            scaling_mode: ScalingMode::FixedVertical {
61                viewport_height: camera_settings.orthographic_viewport_height,
62            },
63            // This is the default value for scale for orthographic projections.
64            // To zoom in and out, change this value, rather than `ScalingMode` or the camera's position.
65            scale: 1.,
66            ..OrthographicProjection::default_3d()
67        }),
68        Transform::from_xyz(5.0, 5.0, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
69    ));
70
71    commands.spawn((
72        Name::new("Plane"),
73        Mesh3d(meshes.add(Plane3d::default().mesh().size(5.0, 5.0))),
74        MeshMaterial3d(materials.add(StandardMaterial {
75            base_color: Color::srgb(0.3, 0.5, 0.3),
76            // Turning off culling keeps the plane visible when viewed from beneath.
77            cull_mode: None,
78            ..default()
79        })),
80    ));
81
82    commands.spawn((
83        Name::new("Fox"),
84        WorldAssetRoot(
85            asset_server.load(GltfAssetLabel::Scene(0).from_asset("models/animated/Fox.glb")),
86        ),
87        // Note: the scale adjustment is purely an accident of our fox model, which renders
88        // HUGE unless mitigated!
89        Transform::from_translation(Vec3::splat(0.0)).with_scale(Vec3::splat(0.025)),
90    ));
91
92    commands.spawn((
93        Name::new("Light"),
94        PointLight::default(),
95        Transform::from_xyz(3.0, 8.0, 5.0),
96    ));
97}
98
99fn instructions(mut commands: Commands) {
100    commands.spawn((
101        Name::new("Instructions"),
102        Text::new(
103            "Scroll mouse wheel to zoom in/out\n\
104            Space: switch between orthographic and perspective projections",
105        ),
106        Node {
107            position_type: PositionType::Absolute,
108            top: px(12),
109            left: px(12),
110            ..default()
111        },
112    ));
113}
114
115fn switch_projection(
116    mut camera: Single<&mut Projection, With<Camera>>,
117    camera_settings: Res<CameraSettings>,
118    keyboard_input: Res<ButtonInput<KeyCode>>,
119) {
120    if keyboard_input.just_pressed(KeyCode::Space) {
121        // Switch projection type
122        **camera = match **camera {
123            Projection::Orthographic(_) => Projection::Perspective(PerspectiveProjection {
124                fov: camera_settings.perspective_zoom_range.start,
125                ..default()
126            }),
127            Projection::Perspective(_) => Projection::Orthographic(OrthographicProjection {
128                scaling_mode: ScalingMode::FixedVertical {
129                    viewport_height: camera_settings.orthographic_viewport_height,
130                },
131                ..OrthographicProjection::default_3d()
132            }),
133            _ => return,
134        }
135    }
136}
137
138fn zoom(
139    camera: Single<&mut Projection, With<Camera>>,
140    camera_settings: Res<CameraSettings>,
141    mouse_wheel_input: Res<AccumulatedMouseScroll>,
142    scroll_conversion: Res<MouseScrollPixelsPerLine>,
143) {
144    // Usually, you won't need to handle both types of projection,
145    // but doing so makes for a more complete example.
146
147    // Get a scroll amount proportional to the kind of input that generated it.
148    let scroll = mouse_wheel_input.to_lines(&scroll_conversion).delta.y;
149
150    match *camera.into_inner() {
151        Projection::Orthographic(ref mut orthographic) => {
152            // We want scrolling up to zoom in, decreasing the scale, so we negate the delta.
153            let delta_zoom = -scroll * camera_settings.orthographic_zoom_speed;
154            // When changing scales, logarithmic changes are more intuitive.
155            // To get this effect, we add 1 to the delta, so that a delta of 0
156            // results in no multiplicative effect, positive values result in a multiplicative increase,
157            // and negative values result in multiplicative decreases.
158            let multiplicative_zoom = 1. + delta_zoom;
159
160            orthographic.scale = (orthographic.scale * multiplicative_zoom).clamp(
161                camera_settings.orthographic_zoom_range.start,
162                camera_settings.orthographic_zoom_range.end,
163            );
164        }
165        Projection::Perspective(ref mut perspective) => {
166            // We want scrolling up to zoom in, decreasing the scale, so we negate the delta.
167            let delta_zoom = -scroll * camera_settings.perspective_zoom_speed;
168
169            // Adjust the field of view, but keep it within our stated range.
170            perspective.fov = (perspective.fov + delta_zoom).clamp(
171                camera_settings.perspective_zoom_range.start,
172                camera_settings.perspective_zoom_range.end,
173            );
174        }
175        _ => (),
176    }
177}