Sprite slicing rendering issue using nearest scaling and changing window size
- Dominant language
- Rust
- Stars
- 48.2k
- Forks
- 4.8k
- Avg merge
- 3d 22h
- Merged PRs (30d)
- 161
Description
## Bevy version
0.16.1
## \[Optional\] Relevant system information
Windows 11, Nvidia
```ignore
`AdapterInfo { name: "NVIDIA GeForce RTX 3060 Ti", vendor: 4318, device: 9353, device_type: DiscreteGpu, driver: "NVIDIA", driver_info: "576.28", backend: Vulkan }`
```
## What you did
Create a nine-sliced sprite in the world, set the `ImagePlugin` to use nearest filtering for clean pixel art.
I created a minimal version of the problem based roughly off of the 2d `sprite_slice` example and using a custom image.
## What went wrong
I expected that the appearance of the sprite would remain consistent no matter the size of the window or the scale factor of the camera. However, when the window is resized, the sprites appearance breaks, leading to half pixels, pixels in the wrong position and general artefacts. Sometimes the image appears correctly, and other times it is broken.
- what were you expecting?

- what actually happened?

## Additional information
After some poking around at the suggestion of @laundmo in the Discord using `bevy_inspector_ecs` I was able to see that the broken rendering matched up with uneven values of the `area` field on the camera entity's `projection` component. Whenever any of the `min` or `max` fields show a value ending in anything buy `.0` or `.5` there will be artefacts. It also sometimes occurs when changing the scale factor of the camera, although it is still most obvious when grabbing and resizing the window manually.
I'd also add the caveat that this is my first time trying to use nine sliced sprites, so it is possible that my technique is unideal in code/art, but I have tried several changes that made no discernible difference.
The minimal example I used to produce this:
```rust
use bevy::{prelude::*, sprite::Anchor};
const HORIZONTAL_TILES: f32 = 7.0;
const VERTICAL_TILES: f32 = 3.0;
const TILE_SIZE: f32 = 64.0;
fn main() {
let mut app = App::new();
app.add_plugins(DefaultPlugins.set(ImagePlugin::default_nearest()));
app.add_systems(Startup, spawn_camera);
app.add_systems(Startup, draw_background);
app.run();
}
fn draw_background(mut commands: Commands, asset_server: Res) {
let background_handle = asset_server.load("background.png");
let slicer = TextureSlicer {
border: BorderRect::all(64.0),
center_scale_mode: SliceScaleMode::Tile { stretch_value: 1.0 },
sides_scale_mode: SliceScaleMode::Tile { stretch_value: 1.0 },
max_corner_scale: 1.0,
};
let size = Vec2::new(HORIZONTAL_TILES * TILE_SIZE, VERTICAL_TILES * TILE_SIZE);
let origin = -(size / 2.0).extend(0.0); // To match the bottom left anchor point.
commands.spawn((
Anchor::BOTTOM_LEFT,
Sprite {
image: background_handle.clone(),
custom_size: Some(size),
image_mode: SpriteImageMode::Sliced(slicer),
..default()
},
Transform::from_translation(origin),
));
}
fn spawn_camera(mut commands: Commands) {
commands.spawn((
Camera2d,
Projection::from(OrthographicProjection {
scale: 0.5, // To make the 1 pixel lines readable.
..OrthographicProjection::default_2d()
}),
));
}
```
The original image:

Contributor guide
Assessment
This issue has not been assessed yet.