Fix 2D looking blurry at odd window sizes (#13440)

# Objective

This is a long-standing bug that I have experienced since many versions
of Bevy ago, possibly forever. Today I finally wanted to report it, but
the fix was so easy that I just went and fixed it. :)

The problem is that 2D graphics looks blurry at odd-sized window
resolutions. This is with the **default** 2D camera configuration! The
issue will also manifest itself with any Orthographic Projection with
`ScalingMode::WindowSize` where the viewport origin is not at one of the
corners, such as the default where the origin point is at the center.

The issue happens because the Bevy orthographic projection origin point
is specified as a fraction to be multiplied by the size. For example,
the default (origin at center) is `(0.5, 0.5)`. When this value is
multiplied by the window size, it can result in fractional values for
the actual origin of the projection, thus placing the camera "between
pixels" and misaligning the entire pixel grid.

With the default value, this happens at odd-numbered window resolutions.
It is very easy to reproduce the issue by running any Bevy 2D app with a
resizable window, and slowly resizing the window pixel by pixel. As you
move the mouse to resize the window, you can see how the 2D graphics
inside the window alternate between "crisp, blurry, crisp, blurry, ...".
If you change the projection's origin to be at the corner (say, `(0.0,
0.0)`) and run the app again, the graphics always looks crisp,
regardless of window size.

Here are screenshots from **before** this PR, to illustrate the issue:

Even window size:

![Screenshot_20240520_165304](https://github.com/bevyengine/bevy/assets/40234599/52619281-cf5f-490e-b85e-22bc5f9af737)

Odd window size:

![Screenshot_20240520_165320](https://github.com/bevyengine/bevy/assets/40234599/27a3624c-f39e-4493-ade9-ca3533802083)


## Solution

The solution is easy: just round the computed origin values for the
projection.

To make it work reliably for the general case, I decided to:
- Only do it for `ScalingMode::WindowSize`, as it doesn't make sense for
other scaling modes.
- Round to the nearest multiple of the pixel scale, if it is not 1.0.
This ensures the "pixels" stay aligned even if scaled.

## Testing

I ran Bevy's examples as well as my own projects to ensure things look
correct. I set different values for the pixel scale to test the rounding
behavior and played around with resizing the window to verify that
everything is consistent.

---

## Changelog

Fixed:
- Orthographic projection now rounds the origin point if computed from
screen pixels, so that 2D graphics do not appear blurry at odd window
sizes.
This commit is contained in:
Ida "Iyes 2024-05-22 04:59:40 +02:00 committed by GitHub
parent 182fe3292e
commit 60afec2a00
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -440,8 +440,16 @@ impl CameraProjection for OrthographicProjection {
ScalingMode::Fixed { width, height } => (width, height), ScalingMode::Fixed { width, height } => (width, height),
}; };
let origin_x = projection_width * self.viewport_origin.x; let mut origin_x = projection_width * self.viewport_origin.x;
let origin_y = projection_height * self.viewport_origin.y; let mut origin_y = projection_height * self.viewport_origin.y;
// If projection is based on window pixels,
// ensure we don't end up with fractional pixels!
if let ScalingMode::WindowSize(pixel_scale) = self.scaling_mode {
// round to nearest multiple of `pixel_scale`
origin_x = (origin_x * pixel_scale).round() / pixel_scale;
origin_y = (origin_y * pixel_scale).round() / pixel_scale;
}
self.area = Rect::new( self.area = Rect::new(
self.scale * -origin_x, self.scale * -origin_x,