Skip to content

Commit 2bd3282

Browse files
Improve API for scaling orthographic cameras (#15969)
# Objective Fixes #15791. As raised in #11022, scaling orthographic cameras is confusing! In Bevy 0.14, there were multiple completely redundant ways to do this, and no clear guidance on which to use. As a result, #15075 removed the `scale` field from `OrthographicProjection` completely, solving the redundancy issue. However, this resulted in an unintuitive API and a painful migration, as discussed in #15791. Users simply want to change a single parameter to zoom, rather than deal with the irrelevant details of how the camera is being scaled. ## Solution This PR reverts #15075, and takes an alternate, more nuanced approach to the redundancy problem. `ScalingMode::WindowSize` was by far the biggest offender. This was the default variant, and stored a float that was *fully* redundant to setting `scale`. All of the other variants contained meaningful semantic information and had an intuitive scale. I could have made these unitless, storing an aspect ratio, but this would have been a worse API and resulted in a pointlessly painful migration. In the course of this work I've also: - improved the documentation to explain that you should just set `scale` to zoom cameras - swapped to named fields for all of the variants in `ScalingMode` for more clarity about the parameter meanings - substantially improved the `projection_zoom` example - removed the footgunny `Mul` and `Div` impls for `ScalingMode`, especially since these no longer have the intended effect on `ScalingMode::WindowSize`. - removed a rounding step because this is now redundant 🎉 ## Testing I've tested these changes as part of my work in the `projection_zoom` example, and things seem to work fine. ## Migration Guide `ScalingMode` has been refactored for clarity, especially on how to zoom orthographic cameras and their projections: - `ScalingMode::WindowSize` no longer stores a float, and acts as if its value was 1. Divide your camera's scale by any previous value to achieve identical results. - `ScalingMode::FixedVertical` and `FixedHorizontal` now use named fields. --------- Co-authored-by: MiniaczQ <[email protected]>
1 parent 90b5ed6 commit 2bd3282

File tree

9 files changed

+139
-145
lines changed

9 files changed

+139
-145
lines changed

crates/bevy_gltf/src/loader.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -1398,7 +1398,9 @@ fn load_node(
13981398
let orthographic_projection = OrthographicProjection {
13991399
near: orthographic.znear(),
14001400
far: orthographic.zfar(),
1401-
scaling_mode: ScalingMode::FixedHorizontal(xmag),
1401+
scaling_mode: ScalingMode::FixedHorizontal {
1402+
viewport_width: xmag,
1403+
},
14021404
..OrthographicProjection::default_3d()
14031405
};
14041406

crates/bevy_render/src/camera/projection.rs

+57-86
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
use core::{
2-
marker::PhantomData,
3-
ops::{Div, DivAssign, Mul, MulAssign},
4-
};
1+
use core::marker::PhantomData;
52

63
use crate::{primitives::Frustum, view::VisibilitySystems};
74
use bevy_app::{App, Plugin, PostStartup, PostUpdate};
@@ -269,92 +266,56 @@ impl Default for PerspectiveProjection {
269266

270267
/// Scaling mode for [`OrthographicProjection`].
271268
///
269+
/// The effect of these scaling modes are combined with the [`OrthographicProjection::scale`] property.
270+
///
271+
/// For example, if the scaling mode is `ScalingMode::Fixed { width: 100.0, height: 300 }` and the scale is `2.0`,
272+
/// the projection will be 200 world units wide and 600 world units tall.
273+
///
272274
/// # Examples
273275
///
274276
/// Configure the orthographic projection to two world units per window height:
275277
///
276278
/// ```
277279
/// # use bevy_render::camera::{OrthographicProjection, Projection, ScalingMode};
278280
/// let projection = Projection::Orthographic(OrthographicProjection {
279-
/// scaling_mode: ScalingMode::FixedVertical(2.0),
281+
/// scaling_mode: ScalingMode::FixedVertical { viewport_height: 2.0 },
280282
/// ..OrthographicProjection::default_2d()
281283
/// });
282284
/// ```
283-
#[derive(Debug, Clone, Copy, Reflect, Serialize, Deserialize)]
285+
#[derive(Default, Debug, Clone, Copy, Reflect, Serialize, Deserialize)]
284286
#[reflect(Serialize, Deserialize)]
285287
pub enum ScalingMode {
288+
/// Match the viewport size.
289+
///
290+
/// With a scale of 1, lengths in world units will map 1:1 with the number of pixels used to render it.
291+
/// For example, if we have a 64x64 sprite with a [`Transform::scale`](bevy_transform::prelude::Transform) of 1.0,
292+
/// no custom size and no inherited scale, the sprite will be 64 world units wide and 64 world units tall.
293+
/// When rendered with [`OrthographicProjection::scaling_mode`] set to `WindowSize` when the window scale factor is 1
294+
/// the sprite will be rendered at 64 pixels wide and 64 pixels tall.
295+
///
296+
/// Changing any of these properties will multiplicatively affect the final size.
297+
#[default]
298+
WindowSize,
286299
/// Manually specify the projection's size, ignoring window resizing. The image will stretch.
287-
/// Arguments are in world units.
300+
///
301+
/// Arguments describe the area of the world that is shown (in world units).
288302
Fixed { width: f32, height: f32 },
289-
/// Match the viewport size.
290-
/// The argument is the number of pixels that equals one world unit.
291-
WindowSize(f32),
292303
/// Keeping the aspect ratio while the axes can't be smaller than given minimum.
304+
///
293305
/// Arguments are in world units.
294306
AutoMin { min_width: f32, min_height: f32 },
295307
/// Keeping the aspect ratio while the axes can't be bigger than given maximum.
308+
///
296309
/// Arguments are in world units.
297310
AutoMax { max_width: f32, max_height: f32 },
298311
/// Keep the projection's height constant; width will be adjusted to match aspect ratio.
312+
///
299313
/// The argument is the desired height of the projection in world units.
300-
FixedVertical(f32),
314+
FixedVertical { viewport_height: f32 },
301315
/// Keep the projection's width constant; height will be adjusted to match aspect ratio.
316+
///
302317
/// The argument is the desired width of the projection in world units.
303-
FixedHorizontal(f32),
304-
}
305-
306-
impl Mul<f32> for ScalingMode {
307-
type Output = ScalingMode;
308-
309-
/// Scale the `ScalingMode`. For example, multiplying by 2 makes the viewport twice as large.
310-
fn mul(self, rhs: f32) -> ScalingMode {
311-
match self {
312-
ScalingMode::Fixed { width, height } => ScalingMode::Fixed {
313-
width: width * rhs,
314-
height: height * rhs,
315-
},
316-
ScalingMode::WindowSize(pixels_per_world_unit) => {
317-
ScalingMode::WindowSize(pixels_per_world_unit / rhs)
318-
}
319-
ScalingMode::AutoMin {
320-
min_width,
321-
min_height,
322-
} => ScalingMode::AutoMin {
323-
min_width: min_width * rhs,
324-
min_height: min_height * rhs,
325-
},
326-
ScalingMode::AutoMax {
327-
max_width,
328-
max_height,
329-
} => ScalingMode::AutoMax {
330-
max_width: max_width * rhs,
331-
max_height: max_height * rhs,
332-
},
333-
ScalingMode::FixedVertical(size) => ScalingMode::FixedVertical(size * rhs),
334-
ScalingMode::FixedHorizontal(size) => ScalingMode::FixedHorizontal(size * rhs),
335-
}
336-
}
337-
}
338-
339-
impl MulAssign<f32> for ScalingMode {
340-
fn mul_assign(&mut self, rhs: f32) {
341-
*self = *self * rhs;
342-
}
343-
}
344-
345-
impl Div<f32> for ScalingMode {
346-
type Output = ScalingMode;
347-
348-
/// Scale the `ScalingMode`. For example, dividing by 2 makes the viewport half as large.
349-
fn div(self, rhs: f32) -> ScalingMode {
350-
self * (1.0 / rhs)
351-
}
352-
}
353-
354-
impl DivAssign<f32> for ScalingMode {
355-
fn div_assign(&mut self, rhs: f32) {
356-
*self = *self / rhs;
357-
}
318+
FixedHorizontal { viewport_width: f32 },
358319
}
359320

360321
/// Project a 3D space onto a 2D surface using parallel lines, i.e., unlike [`PerspectiveProjection`],
@@ -373,7 +334,8 @@ impl DivAssign<f32> for ScalingMode {
373334
/// ```
374335
/// # use bevy_render::camera::{OrthographicProjection, Projection, ScalingMode};
375336
/// let projection = Projection::Orthographic(OrthographicProjection {
376-
/// scaling_mode: ScalingMode::WindowSize(100.0),
337+
/// scaling_mode: ScalingMode::WindowSize,
338+
/// scale: 0.01,
377339
/// ..OrthographicProjection::default_2d()
378340
/// });
379341
/// ```
@@ -407,8 +369,24 @@ pub struct OrthographicProjection {
407369
pub viewport_origin: Vec2,
408370
/// How the projection will scale to the viewport.
409371
///
410-
/// Defaults to `ScalingMode::WindowSize(1.0)`
372+
/// Defaults to [`ScalingMode::WindowSize`],
373+
/// and works in concert with [`OrthographicProjection::scale`] to determine the final effect.
374+
///
375+
/// For simplicity, zooming should be done by changing [`OrthographicProjection::scale`],
376+
/// rather than changing the parameters of the scaling mode.
411377
pub scaling_mode: ScalingMode,
378+
/// Scales the projection.
379+
///
380+
/// As scale increases, the apparent size of objects decreases, and vice versa.
381+
///
382+
/// Note: scaling can be set by [`scaling_mode`](Self::scaling_mode) as well.
383+
/// This parameter scales on top of that.
384+
///
385+
/// This property is particularly useful in implementing zoom functionality.
386+
///
387+
/// Defaults to `1.0`, which under standard settings corresponds to a 1:1 mapping of world units to rendered pixels.
388+
/// See [`ScalingMode::WindowSize`] for more information.
389+
pub scale: f32,
412390
/// The area that the projection covers relative to `viewport_origin`.
413391
///
414392
/// Bevy's [`camera_system`](crate::camera::camera_system) automatically
@@ -478,7 +456,7 @@ impl CameraProjection for OrthographicProjection {
478456

479457
fn update(&mut self, width: f32, height: f32) {
480458
let (projection_width, projection_height) = match self.scaling_mode {
481-
ScalingMode::WindowSize(pixel_scale) => (width / pixel_scale, height / pixel_scale),
459+
ScalingMode::WindowSize => (width, height),
482460
ScalingMode::AutoMin {
483461
min_width,
484462
min_height,
@@ -503,31 +481,23 @@ impl CameraProjection for OrthographicProjection {
503481
(max_width, height * max_width / width)
504482
}
505483
}
506-
ScalingMode::FixedVertical(viewport_height) => {
484+
ScalingMode::FixedVertical { viewport_height } => {
507485
(width * viewport_height / height, viewport_height)
508486
}
509-
ScalingMode::FixedHorizontal(viewport_width) => {
487+
ScalingMode::FixedHorizontal { viewport_width } => {
510488
(viewport_width, height * viewport_width / width)
511489
}
512490
ScalingMode::Fixed { width, height } => (width, height),
513491
};
514492

515-
let mut origin_x = projection_width * self.viewport_origin.x;
516-
let mut origin_y = projection_height * self.viewport_origin.y;
517-
518-
// If projection is based on window pixels,
519-
// ensure we don't end up with fractional pixels!
520-
if let ScalingMode::WindowSize(pixel_scale) = self.scaling_mode {
521-
// round to nearest multiple of `pixel_scale`
522-
origin_x = (origin_x * pixel_scale).round() / pixel_scale;
523-
origin_y = (origin_y * pixel_scale).round() / pixel_scale;
524-
}
493+
let origin_x = projection_width * self.viewport_origin.x;
494+
let origin_y = projection_height * self.viewport_origin.y;
525495

526496
self.area = Rect::new(
527-
-origin_x,
528-
-origin_y,
529-
projection_width - origin_x,
530-
projection_height - origin_y,
497+
self.scale * -origin_x,
498+
self.scale * -origin_y,
499+
self.scale * (projection_width - origin_x),
500+
self.scale * (projection_height - origin_y),
531501
);
532502
}
533503

@@ -575,10 +545,11 @@ impl OrthographicProjection {
575545
/// objects that are behind it.
576546
pub fn default_3d() -> Self {
577547
OrthographicProjection {
548+
scale: 1.0,
578549
near: 0.0,
579550
far: 1000.0,
580551
viewport_origin: Vec2::new(0.5, 0.5),
581-
scaling_mode: ScalingMode::WindowSize(1.0),
552+
scaling_mode: ScalingMode::WindowSize,
582553
area: Rect::new(-1.0, -1.0, 1.0, 1.0),
583554
}
584555
}

examples/2d/pixel_grid_snap.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
use bevy::{
44
prelude::*,
55
render::{
6-
camera::{RenderTarget, ScalingMode},
6+
camera::RenderTarget,
77
render_resource::{
88
Extent3d, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages,
99
},
@@ -149,6 +149,6 @@ fn fit_canvas(
149149
for event in resize_events.read() {
150150
let h_scale = event.width / RES_WIDTH as f32;
151151
let v_scale = event.height / RES_HEIGHT as f32;
152-
projection.scaling_mode = ScalingMode::WindowSize(h_scale.min(v_scale).round());
152+
projection.scale = 1. / h_scale.min(v_scale).round();
153153
}
154154
}

examples/3d/camera_sub_view.rs

+12-4
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,9 @@ fn setup(
141141
commands.spawn((
142142
Camera3d::default(),
143143
Projection::from(OrthographicProjection {
144-
scaling_mode: ScalingMode::FixedVertical(6.0),
144+
scaling_mode: ScalingMode::FixedVertical {
145+
viewport_height: 6.0,
146+
},
145147
..OrthographicProjection::default_3d()
146148
}),
147149
Camera {
@@ -161,7 +163,9 @@ fn setup(
161163
commands.spawn((
162164
Camera3d::default(),
163165
Projection::from(OrthographicProjection {
164-
scaling_mode: ScalingMode::FixedVertical(6.0),
166+
scaling_mode: ScalingMode::FixedVertical {
167+
viewport_height: 6.0,
168+
},
165169
..OrthographicProjection::default_3d()
166170
}),
167171
Camera {
@@ -187,7 +191,9 @@ fn setup(
187191
commands.spawn((
188192
Camera3d::default(),
189193
Projection::from(OrthographicProjection {
190-
scaling_mode: ScalingMode::FixedVertical(6.0),
194+
scaling_mode: ScalingMode::FixedVertical {
195+
viewport_height: 6.0,
196+
},
191197
..OrthographicProjection::default_3d()
192198
}),
193199
Camera {
@@ -214,7 +220,9 @@ fn setup(
214220
commands.spawn((
215221
Camera3d::default(),
216222
Projection::from(OrthographicProjection {
217-
scaling_mode: ScalingMode::FixedVertical(6.0),
223+
scaling_mode: ScalingMode::FixedVertical {
224+
viewport_height: 6.0,
225+
},
218226
..OrthographicProjection::default_3d()
219227
}),
220228
Camera {

examples/3d/orthographic.rs

+4-2
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@ fn setup(
1919
commands.spawn((
2020
Camera3d::default(),
2121
Projection::from(OrthographicProjection {
22-
// 6 world units per window height.
23-
scaling_mode: ScalingMode::FixedVertical(6.0),
22+
// 6 world units per pixel of window height.
23+
scaling_mode: ScalingMode::FixedVertical {
24+
viewport_height: 6.0,
25+
},
2426
..OrthographicProjection::default_3d()
2527
}),
2628
Transform::from_xyz(5.0, 5.0, 5.0).looking_at(Vec3::ZERO, Vec3::Y),

examples/3d/pbr.rs

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//! This example shows how to configure Physically Based Rendering (PBR) parameters.
22
3-
use bevy::{prelude::*, render::camera::ScalingMode};
3+
use bevy::prelude::*;
4+
use bevy::render::camera::ScalingMode;
45

56
fn main() {
67
App::new()
@@ -110,7 +111,8 @@ fn setup(
110111
Camera3d::default(),
111112
Transform::from_xyz(0.0, 0.0, 8.0).looking_at(Vec3::default(), Vec3::Y),
112113
Projection::from(OrthographicProjection {
113-
scaling_mode: ScalingMode::WindowSize(100.0),
114+
scale: 100.,
115+
scaling_mode: ScalingMode::WindowSize,
114116
..OrthographicProjection::default_3d()
115117
}),
116118
EnvironmentMapLight {

0 commit comments

Comments
 (0)