
# Objective I'm building a bloxel game in which I (currently) use a texture atlas to render the blocks the world is made of. While I was coding it, I was using the `TextureAtlas...` types to build the terrain's texture atlas at runtime as shown in the [example](https://github.com/bevyengine/bevy/blob/latest/examples/2d/texture_atlas.rs). But when I was using it to build a 3D mesh out of the blocks, I found that there was no easy way get the texture rect in UV coordinates, only in pixels via `texture_rect()`. I had to resort to writing code like this: ```rs let size = layout.size.as_vec2(); if let Some(rect) = sources.texture_rect(layout, texture) { let rect = rect.as_rect(); let uvs = Rect::from_corners(rect.min / size, rect.max / size); // use the UVs here, such as to build vertex buffer } ``` That is, until I wrote a helper function that's practically identical to the one in this PR. ## Solution Add a `uv_rect` function to `TextureAtlasSources` that will return a `Rect` with coordinates in the range of 0.0 to 1.0 – that is, UV coordinates – which can then be used directly to build `Vec2` UV values to put into a buffer and send to the GPU. I'm a little unsure about the wording of the `texture_rect` documentation but I kept it intact and based mine on it. If you think this could be improved and have some advice, I'd love to include that in this PR. ## Testing I've not done any testing with the updated bevy branch, other than seeing that the original helper function (identical in functionality) worked in my currently very small project, and making sure `cargo build` doesn't error, but I'm new to making changes to Bevy so unsure if this is sufficient. ## Showcase 
228 lines
8.0 KiB
Rust
228 lines
8.0 KiB
Rust
use bevy_app::prelude::*;
|
|
use bevy_asset::{Asset, AssetApp as _, AssetId, Assets, Handle};
|
|
use bevy_math::{Rect, URect, UVec2};
|
|
use bevy_platform_support::collections::HashMap;
|
|
#[cfg(feature = "bevy_reflect")]
|
|
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
|
|
#[cfg(feature = "serialize")]
|
|
use bevy_reflect::{ReflectDeserialize, ReflectSerialize};
|
|
|
|
use crate::Image;
|
|
|
|
/// Adds support for texture atlases.
|
|
pub struct TextureAtlasPlugin;
|
|
|
|
impl Plugin for TextureAtlasPlugin {
|
|
fn build(&self, app: &mut App) {
|
|
app.init_asset::<TextureAtlasLayout>();
|
|
|
|
#[cfg(feature = "bevy_reflect")]
|
|
app.register_asset_reflect::<TextureAtlasLayout>()
|
|
.register_type::<TextureAtlas>();
|
|
}
|
|
}
|
|
|
|
/// Stores a mapping from sub texture handles to the related area index.
|
|
///
|
|
/// Generated by [`TextureAtlasBuilder`].
|
|
///
|
|
/// [`TextureAtlasBuilder`]: crate::TextureAtlasBuilder
|
|
#[derive(Debug)]
|
|
pub struct TextureAtlasSources {
|
|
/// Maps from a specific image handle to the index in `textures` where they can be found.
|
|
pub texture_ids: HashMap<AssetId<Image>, usize>,
|
|
}
|
|
impl TextureAtlasSources {
|
|
/// Retrieves the texture *section* index of the given `texture` handle.
|
|
pub fn texture_index(&self, texture: impl Into<AssetId<Image>>) -> Option<usize> {
|
|
let id = texture.into();
|
|
self.texture_ids.get(&id).cloned()
|
|
}
|
|
|
|
/// Creates a [`TextureAtlas`] handle for the given `texture` handle.
|
|
pub fn handle(
|
|
&self,
|
|
layout: Handle<TextureAtlasLayout>,
|
|
texture: impl Into<AssetId<Image>>,
|
|
) -> Option<TextureAtlas> {
|
|
Some(TextureAtlas {
|
|
layout,
|
|
index: self.texture_index(texture)?,
|
|
})
|
|
}
|
|
|
|
/// Retrieves the texture *section* rectangle of the given `texture` handle in pixels.
|
|
pub fn texture_rect(
|
|
&self,
|
|
layout: &TextureAtlasLayout,
|
|
texture: impl Into<AssetId<Image>>,
|
|
) -> Option<URect> {
|
|
layout.textures.get(self.texture_index(texture)?).cloned()
|
|
}
|
|
|
|
/// Retrieves the texture *section* rectangle of the given `texture` handle in UV coordinates.
|
|
/// These are within the range [0..1], as a fraction of the entire texture atlas' size.
|
|
pub fn uv_rect(
|
|
&self,
|
|
layout: &TextureAtlasLayout,
|
|
texture: impl Into<AssetId<Image>>,
|
|
) -> Option<Rect> {
|
|
self.texture_rect(layout, texture).map(|rect| {
|
|
let rect = rect.as_rect();
|
|
let size = layout.size.as_vec2();
|
|
Rect::from_corners(rect.min / size, rect.max / size)
|
|
})
|
|
}
|
|
}
|
|
|
|
/// Stores a map used to lookup the position of a texture in a [`TextureAtlas`].
|
|
/// This can be used to either use and look up a specific section of a texture, or animate frame-by-frame as a sprite sheet.
|
|
///
|
|
/// Optionally it can store a mapping from sub texture handles to the related area index (see
|
|
/// [`TextureAtlasBuilder`]).
|
|
///
|
|
/// [Example usage animating sprite.](https://github.com/bevyengine/bevy/blob/latest/examples/2d/sprite_sheet.rs)
|
|
/// [Example usage animating sprite in response to an event.](https://github.com/bevyengine/bevy/blob/latest/examples/2d/sprite_animation.rs)
|
|
/// [Example usage loading sprite sheet.](https://github.com/bevyengine/bevy/blob/latest/examples/2d/texture_atlas.rs)
|
|
///
|
|
/// [`TextureAtlasBuilder`]: crate::TextureAtlasBuilder
|
|
#[derive(Asset, PartialEq, Eq, Debug, Clone)]
|
|
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, PartialEq))]
|
|
#[cfg_attr(
|
|
feature = "serialize",
|
|
derive(serde::Serialize, serde::Deserialize),
|
|
reflect(Serialize, Deserialize)
|
|
)]
|
|
pub struct TextureAtlasLayout {
|
|
/// Total size of texture atlas.
|
|
pub size: UVec2,
|
|
/// The specific areas of the atlas where each texture can be found
|
|
pub textures: Vec<URect>,
|
|
}
|
|
|
|
impl TextureAtlasLayout {
|
|
/// Create a new empty layout with custom `dimensions`
|
|
pub fn new_empty(dimensions: UVec2) -> Self {
|
|
Self {
|
|
size: dimensions,
|
|
textures: Vec::new(),
|
|
}
|
|
}
|
|
|
|
/// Generate a [`TextureAtlasLayout`] as a grid where each
|
|
/// `tile_size` by `tile_size` grid-cell is one of the *section* in the
|
|
/// atlas. Grid cells are separated by some `padding`, and the grid starts
|
|
/// at `offset` pixels from the top left corner. Resulting layout is
|
|
/// indexed left to right, top to bottom.
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `tile_size` - Each layout grid cell size
|
|
/// * `columns` - Grid column count
|
|
/// * `rows` - Grid row count
|
|
/// * `padding` - Optional padding between cells
|
|
/// * `offset` - Optional global grid offset
|
|
pub fn from_grid(
|
|
tile_size: UVec2,
|
|
columns: u32,
|
|
rows: u32,
|
|
padding: Option<UVec2>,
|
|
offset: Option<UVec2>,
|
|
) -> Self {
|
|
let padding = padding.unwrap_or_default();
|
|
let offset = offset.unwrap_or_default();
|
|
let mut sprites = Vec::new();
|
|
let mut current_padding = UVec2::ZERO;
|
|
|
|
for y in 0..rows {
|
|
if y > 0 {
|
|
current_padding.y = padding.y;
|
|
}
|
|
for x in 0..columns {
|
|
if x > 0 {
|
|
current_padding.x = padding.x;
|
|
}
|
|
|
|
let cell = UVec2::new(x, y);
|
|
let rect_min = (tile_size + current_padding) * cell + offset;
|
|
|
|
sprites.push(URect {
|
|
min: rect_min,
|
|
max: rect_min + tile_size,
|
|
});
|
|
}
|
|
}
|
|
|
|
let grid_size = UVec2::new(columns, rows);
|
|
|
|
Self {
|
|
size: ((tile_size + current_padding) * grid_size) - current_padding,
|
|
textures: sprites,
|
|
}
|
|
}
|
|
|
|
/// Add a *section* to the list in the layout and returns its index
|
|
/// which can be used with [`TextureAtlas`]
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `rect` - The section of the texture to be added
|
|
///
|
|
/// [`TextureAtlas`]: crate::TextureAtlas
|
|
pub fn add_texture(&mut self, rect: URect) -> usize {
|
|
self.textures.push(rect);
|
|
self.textures.len() - 1
|
|
}
|
|
|
|
/// The number of textures in the [`TextureAtlasLayout`]
|
|
pub fn len(&self) -> usize {
|
|
self.textures.len()
|
|
}
|
|
|
|
pub fn is_empty(&self) -> bool {
|
|
self.textures.is_empty()
|
|
}
|
|
}
|
|
|
|
/// An index into a [`TextureAtlasLayout`], which corresponds to a specific section of a texture.
|
|
///
|
|
/// It stores a handle to [`TextureAtlasLayout`] and the index of the current section of the atlas.
|
|
/// The texture atlas contains various *sections* of a given texture, allowing users to have a single
|
|
/// image file for either sprite animation or global mapping.
|
|
/// You can change the texture [`index`](Self::index) of the atlas to animate the sprite or display only a *section* of the texture
|
|
/// for efficient rendering of related game objects.
|
|
///
|
|
/// Check the following examples for usage:
|
|
/// - [`animated sprite sheet example`](https://github.com/bevyengine/bevy/blob/latest/examples/2d/sprite_sheet.rs)
|
|
/// - [`sprite animation event example`](https://github.com/bevyengine/bevy/blob/latest/examples/2d/sprite_animation.rs)
|
|
/// - [`texture atlas example`](https://github.com/bevyengine/bevy/blob/latest/examples/2d/texture_atlas.rs)
|
|
#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)]
|
|
#[cfg_attr(
|
|
feature = "bevy_reflect",
|
|
derive(Reflect),
|
|
reflect(Default, Debug, PartialEq, Hash)
|
|
)]
|
|
pub struct TextureAtlas {
|
|
/// Texture atlas layout handle
|
|
pub layout: Handle<TextureAtlasLayout>,
|
|
/// Texture atlas section index
|
|
pub index: usize,
|
|
}
|
|
|
|
impl TextureAtlas {
|
|
/// Retrieves the current texture [`URect`] of the sprite sheet according to the section `index`
|
|
pub fn texture_rect(&self, texture_atlases: &Assets<TextureAtlasLayout>) -> Option<URect> {
|
|
let atlas = texture_atlases.get(&self.layout)?;
|
|
atlas.textures.get(self.index).copied()
|
|
}
|
|
}
|
|
|
|
impl From<Handle<TextureAtlasLayout>> for TextureAtlas {
|
|
fn from(texture_atlas: Handle<TextureAtlasLayout>) -> Self {
|
|
Self {
|
|
layout: texture_atlas,
|
|
index: 0,
|
|
}
|
|
}
|
|
}
|