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