Skip to content

Commit dc47379

Browse files
authored
Add resize_in_place to Image (#19410)
# Objective Ultimately, I'd like to modify our font atlas creation systems so that they are able to resize the font atlases as more glyphs are added. At the moment, they create a new 512x512 atlas every time one fills up. With large font sizes and many glyphs, your glyphs may end up spread out across several atlases. The goal would be to render text more efficiently, because glyphs spread across fewer textures could benefit more from batching. `AtlasAllocator` already has support for growing atlases, but we don't currently have a way of growing a texture while keeping the pixel data intact. ## Solution Add a new method to `Image`: `resize_in_place` and a test for it. ## Testing Ran the new test, and also a little demo comparing this side-by-side with `resize`. <details> <summary>Expand Code</summary> ```rust //! Testing ground for #19410 use bevy::prelude::*; use bevy_render::render_resource::Extent3d; fn main() { App::new() .add_plugins(DefaultPlugins) .add_systems(Startup, setup) .add_systems(Update, test) .init_resource::<Size>() .insert_resource(FillColor(Hsla::hsl(0.0, 1.0, 0.7))) .run(); } #[derive(Resource, Default)] struct Size(Option<UVec2>); #[derive(Resource)] struct FillColor(Hsla); #[derive(Component)] struct InPlace; fn setup(mut commands: Commands, asset_server: Res<AssetServer>) { commands.spawn(Camera2d); commands.spawn(( Transform::from_xyz(220.0, 0.0, 0.0), Sprite::from_image(asset_server.load("branding/bevy_bird_dark.png")), )); commands.spawn(( InPlace, Transform::from_xyz(-220.0, 0.0, 0.0), Sprite::from_image(asset_server.load("branding/icon.png")), )); } fn test( sprites: Query<(&Sprite, Has<InPlace>)>, mut images: ResMut<Assets<Image>>, mut new_size: ResMut<Size>, mut dir: Local<IVec2>, mut color: ResMut<FillColor>, ) -> Result { for (sprite, in_place) in &sprites { let image = images.get_mut(&sprite.image).ok_or("Image not found")?; let size = new_size.0.get_or_insert(image.size()); if *dir == IVec2::ZERO { *dir = IVec2::splat(1); } *size = size.saturating_add_signed(*dir); if size.x > 400 || size.x < 150 { *dir = *dir * -1; } color.0 = color.0.rotate_hue(1.0); if in_place { image.resize_in_place_2d( Extent3d { width: size.x, height: size.y, ..default() }, &Srgba::from(color.0).to_u8_array(), )?; } else { image.resize(Extent3d { width: size.x, height: size.y, ..default() }); } } Ok(()) } ``` </details> https://github.com/user-attachments/assets/6b2d0ec3-6a6e-4da1-98aa-29e7162f16fa ## Alternatives I think that this might be useful functionality outside of the font atlas scenario, but we *could* just increase the initial font atlas size, make it configurable, and/or size font atlases according to device limits. It's not totally clear to me how to accomplish that last idea.
1 parent b993202 commit dc47379

File tree

1 file changed

+225
-0
lines changed

1 file changed

+225
-0
lines changed

crates/bevy_image/src/image.rs

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -851,6 +851,8 @@ impl Image {
851851

852852
/// Resizes the image to the new size, by removing information or appending 0 to the `data`.
853853
/// Does not properly scale the contents of the image.
854+
///
855+
/// If you need to keep pixel data intact, use [`Image::resize_in_place`].
854856
pub fn resize(&mut self, size: Extent3d) {
855857
self.texture_descriptor.size = size;
856858
if let Some(ref mut data) = self.data {
@@ -878,6 +880,52 @@ impl Image {
878880
self.texture_descriptor.size = new_size;
879881
}
880882

883+
/// Resizes the image to the new size, keeping the pixel data intact, anchored at the top-left.
884+
/// When growing, the new space is filled with 0. When shrinking, the image is clipped.
885+
///
886+
/// For faster resizing when keeping pixel data intact is not important, use [`Image::resize`].
887+
pub fn resize_in_place(&mut self, new_size: Extent3d) -> Result<(), ResizeError> {
888+
let old_size = self.texture_descriptor.size;
889+
let pixel_size = self.texture_descriptor.format.pixel_size();
890+
let byte_len = self.texture_descriptor.format.pixel_size() * new_size.volume();
891+
892+
let Some(ref mut data) = self.data else {
893+
return Err(ResizeError::ImageWithoutData);
894+
};
895+
896+
let mut new: Vec<u8> = vec![0; byte_len];
897+
898+
let copy_width = old_size.width.min(new_size.width) as usize;
899+
let copy_height = old_size.height.min(new_size.height) as usize;
900+
let copy_depth = old_size
901+
.depth_or_array_layers
902+
.min(new_size.depth_or_array_layers) as usize;
903+
904+
let old_row_stride = old_size.width as usize * pixel_size;
905+
let old_layer_stride = old_size.height as usize * old_row_stride;
906+
907+
let new_row_stride = new_size.width as usize * pixel_size;
908+
let new_layer_stride = new_size.height as usize * new_row_stride;
909+
910+
for z in 0..copy_depth {
911+
for y in 0..copy_height {
912+
let old_offset = z * old_layer_stride + y * old_row_stride;
913+
let new_offset = z * new_layer_stride + y * new_row_stride;
914+
915+
let old_range = (old_offset)..(old_offset + copy_width * pixel_size);
916+
let new_range = (new_offset)..(new_offset + copy_width * pixel_size);
917+
918+
new[new_range].copy_from_slice(&data[old_range]);
919+
}
920+
}
921+
922+
self.data = Some(new);
923+
924+
self.texture_descriptor.size = new_size;
925+
926+
Ok(())
927+
}
928+
881929
/// Takes a 2D image containing vertically stacked images of the same size, and reinterprets
882930
/// it as a 2D array texture, where each of the stacked images becomes one layer of the
883931
/// array. This is primarily for use with the `texture2DArray` shader uniform type.
@@ -1540,6 +1588,14 @@ pub enum TextureError {
15401588
IncompleteCubemap,
15411589
}
15421590

1591+
/// An error that occurs when an image cannot be resized.
1592+
#[derive(Error, Debug)]
1593+
pub enum ResizeError {
1594+
/// Failed to resize an Image because it has no data.
1595+
#[error("resize method requires cpu-side image data but none was present")]
1596+
ImageWithoutData,
1597+
}
1598+
15431599
/// The type of a raw image buffer.
15441600
#[derive(Debug)]
15451601
pub enum ImageType<'a> {
@@ -1730,4 +1786,173 @@ mod test {
17301786
image.set_color_at_3d(4, 9, 2, Color::WHITE).unwrap();
17311787
assert!(matches!(image.get_color_at_3d(4, 9, 2), Ok(Color::WHITE)));
17321788
}
1789+
1790+
#[test]
1791+
fn resize_in_place_2d_grow_and_shrink() {
1792+
use bevy_color::ColorToPacked;
1793+
1794+
const INITIAL_FILL: LinearRgba = LinearRgba::BLACK;
1795+
const GROW_FILL: LinearRgba = LinearRgba::NONE;
1796+
1797+
let mut image = Image::new_fill(
1798+
Extent3d {
1799+
width: 2,
1800+
height: 2,
1801+
depth_or_array_layers: 1,
1802+
},
1803+
TextureDimension::D2,
1804+
&INITIAL_FILL.to_u8_array(),
1805+
TextureFormat::Rgba8Unorm,
1806+
RenderAssetUsages::MAIN_WORLD,
1807+
);
1808+
1809+
// Create a test pattern
1810+
1811+
const TEST_PIXELS: [(u32, u32, LinearRgba); 3] = [
1812+
(0, 1, LinearRgba::RED),
1813+
(1, 1, LinearRgba::GREEN),
1814+
(1, 0, LinearRgba::BLUE),
1815+
];
1816+
1817+
for (x, y, color) in &TEST_PIXELS {
1818+
image.set_color_at(*x, *y, Color::from(*color)).unwrap();
1819+
}
1820+
1821+
// Grow image
1822+
image
1823+
.resize_in_place(Extent3d {
1824+
width: 4,
1825+
height: 4,
1826+
depth_or_array_layers: 1,
1827+
})
1828+
.unwrap();
1829+
1830+
// After growing, the test pattern should be the same.
1831+
assert!(matches!(
1832+
image.get_color_at(0, 0),
1833+
Ok(Color::LinearRgba(INITIAL_FILL))
1834+
));
1835+
for (x, y, color) in &TEST_PIXELS {
1836+
assert_eq!(
1837+
image.get_color_at(*x, *y).unwrap(),
1838+
Color::LinearRgba(*color)
1839+
);
1840+
}
1841+
1842+
// Pixels in the newly added area should get filled with zeroes.
1843+
assert!(matches!(
1844+
image.get_color_at(3, 3),
1845+
Ok(Color::LinearRgba(GROW_FILL))
1846+
));
1847+
1848+
// Shrink
1849+
image
1850+
.resize_in_place(Extent3d {
1851+
width: 1,
1852+
height: 1,
1853+
depth_or_array_layers: 1,
1854+
})
1855+
.unwrap();
1856+
1857+
// Images outside of the new dimensions should be clipped
1858+
assert!(image.get_color_at(1, 1).is_err());
1859+
}
1860+
1861+
#[test]
1862+
fn resize_in_place_array_grow_and_shrink() {
1863+
use bevy_color::ColorToPacked;
1864+
1865+
const INITIAL_FILL: LinearRgba = LinearRgba::BLACK;
1866+
const GROW_FILL: LinearRgba = LinearRgba::NONE;
1867+
const LAYERS: u32 = 4;
1868+
1869+
let mut image = Image::new_fill(
1870+
Extent3d {
1871+
width: 2,
1872+
height: 2,
1873+
depth_or_array_layers: LAYERS,
1874+
},
1875+
TextureDimension::D2,
1876+
&INITIAL_FILL.to_u8_array(),
1877+
TextureFormat::Rgba8Unorm,
1878+
RenderAssetUsages::MAIN_WORLD,
1879+
);
1880+
1881+
// Create a test pattern
1882+
1883+
const TEST_PIXELS: [(u32, u32, LinearRgba); 3] = [
1884+
(0, 1, LinearRgba::RED),
1885+
(1, 1, LinearRgba::GREEN),
1886+
(1, 0, LinearRgba::BLUE),
1887+
];
1888+
1889+
for z in 0..LAYERS {
1890+
for (x, y, color) in &TEST_PIXELS {
1891+
image
1892+
.set_color_at_3d(*x, *y, z, Color::from(*color))
1893+
.unwrap();
1894+
}
1895+
}
1896+
1897+
// Grow image
1898+
image
1899+
.resize_in_place(Extent3d {
1900+
width: 4,
1901+
height: 4,
1902+
depth_or_array_layers: LAYERS + 1,
1903+
})
1904+
.unwrap();
1905+
1906+
// After growing, the test pattern should be the same.
1907+
assert!(matches!(
1908+
image.get_color_at(0, 0),
1909+
Ok(Color::LinearRgba(INITIAL_FILL))
1910+
));
1911+
for z in 0..LAYERS {
1912+
for (x, y, color) in &TEST_PIXELS {
1913+
assert_eq!(
1914+
image.get_color_at_3d(*x, *y, z).unwrap(),
1915+
Color::LinearRgba(*color)
1916+
);
1917+
}
1918+
}
1919+
1920+
// Pixels in the newly added area should get filled with zeroes.
1921+
for z in 0..(LAYERS + 1) {
1922+
assert!(matches!(
1923+
image.get_color_at_3d(3, 3, z),
1924+
Ok(Color::LinearRgba(GROW_FILL))
1925+
));
1926+
}
1927+
1928+
// Shrink
1929+
image
1930+
.resize_in_place(Extent3d {
1931+
width: 1,
1932+
height: 1,
1933+
depth_or_array_layers: 1,
1934+
})
1935+
.unwrap();
1936+
1937+
// Images outside of the new dimensions should be clipped
1938+
assert!(image.get_color_at_3d(1, 1, 0).is_err());
1939+
1940+
// Higher layers should no longer be present
1941+
assert!(image.get_color_at_3d(0, 0, 1).is_err());
1942+
1943+
// Grow layers
1944+
image
1945+
.resize_in_place(Extent3d {
1946+
width: 1,
1947+
height: 1,
1948+
depth_or_array_layers: 2,
1949+
})
1950+
.unwrap();
1951+
1952+
// Pixels in the newly added layer should be zeroes.
1953+
assert!(matches!(
1954+
image.get_color_at_3d(0, 0, 1),
1955+
Ok(Color::LinearRgba(GROW_FILL))
1956+
));
1957+
}
17331958
}

0 commit comments

Comments
 (0)