Add Sprite Flipping (#1407)

OK, here's my attempt at sprite flipping. There are a couple of points that I need review/help on, but I think the UX is about ideal:

```rust
        .spawn(SpriteBundle {
            material: materials.add(texture_handle.into()),
            sprite: Sprite {
                // Flip the sprite along the x axis
                flip: SpriteFlip { x: true, y: false },
                ..Default::default()
            },
            ..Default::default()
        });
```

Now for the issues. The big issue is that for some reason, when flipping the UVs on the sprite, there is a light "bleeding" or whatever you call it where the UV tries to sample past the texture boundry and ends up clipping. This is only noticed when resizing the window, though. You can see a screenshot below.

![image](https://user-images.githubusercontent.com/25393315/107098172-397aaa00-67d4-11eb-8e02-c90c820cd70e.png)

I am quite baffled why the texture sampling is overrunning like it is and could use some guidance if anybody knows what might be wrong.

The other issue, which I just worked around, is that I had to remove the `#[render_resources(from_self)]` annotation from the Spritesheet because the `SpriteFlip` render resource wasn't being picked up properly in the shader when using it. I'm not sure what the cause of that was, but by removing the annotation and re-organizing the shader inputs accordingly the problem was fixed.

I'm not sure if this is the most efficient way to do this or if there is a better way, but I wanted to try it out if only for the learning experience. Let me know what you think!
This commit is contained in:
Zicklag 2021-03-03 19:26:45 +00:00
parent e61d7920e3
commit 89217171b4
10 changed files with 178 additions and 19 deletions

View file

@ -98,6 +98,10 @@ path = "examples/2d/contributors.rs"
name = "sprite"
path = "examples/2d/sprite.rs"
[[example]]
name = "sprite_flipping"
path = "examples/2d/sprite_flipping.rs"
[[example]]
name = "sprite_sheet"
path = "examples/2d/sprite_sheet.rs"

View file

@ -109,7 +109,7 @@ pub fn build_sprite_pipeline(shaders: &mut Assets<Shader>) -> PipelineDescriptor
topology: PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: FrontFace::Ccw,
cull_mode: CullMode::None,
cull_mode: CullMode::Back,
polygon_mode: PolygonMode::Fill,
},
..PipelineDescriptor::new(ShaderStages {

View file

@ -13,12 +13,32 @@ layout(set = 0, binding = 0) uniform Camera {
layout(set = 2, binding = 0) uniform Transform {
mat4 Model;
};
layout(set = 2, binding = 1) uniform Sprite_size {
layout(set = 2, binding = 1) uniform Sprite {
vec2 size;
uint flip;
};
void main() {
v_Uv = Vertex_Uv;
vec2 uv = Vertex_Uv;
// Flip the sprite if necessary by flipping the UVs
uint x_flip_bit = 1; // The X flip bit
uint y_flip_bit = 2; // The Y flip bit
// Note: Here we subtract f32::EPSILON from the flipped UV coord. This is due to reasons unknown
// to me (@zicklag ) that causes the uv's to be slightly offset and causes over/under running of
// the sprite UV sampling which is visible when resizing the screen.
float epsilon = 0.00000011920929;
if ((flip & x_flip_bit) == x_flip_bit) {
uv = vec2(1.0 - uv.x - epsilon, uv.y);
}
if ((flip & y_flip_bit) == y_flip_bit) {
uv = vec2(uv.x, 1.0 - uv.y - epsilon);
}
v_Uv = uv;
vec3 position = Vertex_Position * vec3(size, 1.0);
gl_Position = ViewProj * Model * vec4(position, 1.0);
}

View file

@ -31,21 +31,55 @@ layout(set = 2, binding = 0) uniform Transform {
};
layout(set = 2, binding = 1) uniform TextureAtlasSprite {
vec4 TextureAtlasSprite_color;
uint TextureAtlasSprite_index;
vec4 color;
uint index;
uint flip;
};
void main() {
Rect sprite_rect = Textures[TextureAtlasSprite_index];
Rect sprite_rect = Textures[index];
vec2 sprite_dimensions = sprite_rect.end - sprite_rect.begin;
vec3 vertex_position = vec3(Vertex_Position.xy * sprite_dimensions, 0.0);
// Specify the corners of the sprite
vec2 bottom_left = vec2(sprite_rect.begin.x, sprite_rect.end.y);
vec2 top_left = sprite_rect.begin;
vec2 top_right = vec2(sprite_rect.end.x, sprite_rect.begin.y);
vec2 bottom_right = sprite_rect.end;
// Flip the sprite if necessary
uint x_flip_bit = 1;
uint y_flip_bit = 2;
vec2 tmp;
if ((flip & x_flip_bit) == x_flip_bit) {
// Shuffle the corners to flip around x
tmp = bottom_left;
bottom_left = bottom_right;
bottom_right = tmp;
tmp = top_left;
top_left = top_right;
top_right = tmp;
}
if ((flip & y_flip_bit) == y_flip_bit) {
// Shuffle the corners to flip around y
tmp = bottom_left;
bottom_left = top_left;
top_left = tmp;
tmp = bottom_right;
bottom_right = top_right;
top_right = tmp;
}
vec2 atlas_positions[4] = vec2[](
vec2(sprite_rect.begin.x, sprite_rect.end.y),
sprite_rect.begin,
vec2(sprite_rect.end.x, sprite_rect.begin.y),
sprite_rect.end
bottom_left,
top_left,
top_right,
bottom_right
);
v_Uv = (atlas_positions[gl_VertexIndex]) / AtlasSize;
v_Color = TextureAtlasSprite_color;
v_Color = color;
gl_Position = ViewProj * SpriteTransform * vec4(vertex_position, 1.0);
}

View file

@ -1,19 +1,52 @@
use crate::ColorMaterial;
use bevy_asset::{Assets, Handle};
use bevy_core::Bytes;
use bevy_ecs::{Query, Res};
use bevy_math::Vec2;
use bevy_reflect::{Reflect, ReflectDeserialize, TypeUuid};
use bevy_render::{renderer::RenderResources, texture::Texture};
use bevy_render::{
renderer::{RenderResource, RenderResourceType, RenderResources},
texture::Texture,
};
use serde::{Deserialize, Serialize};
#[derive(Debug, Default, Clone, RenderResources, TypeUuid, Reflect)]
#[derive(Debug, Default, Clone, TypeUuid, Reflect, RenderResources)]
#[render_resources(from_self)]
#[uuid = "7233c597-ccfa-411f-bd59-9af349432ada"]
#[repr(C)]
pub struct Sprite {
pub size: Vec2,
#[render_resources(ignore)]
pub flip_x: bool,
pub flip_y: bool,
pub resize_mode: SpriteResizeMode,
}
impl RenderResource for Sprite {
fn resource_type(&self) -> Option<RenderResourceType> {
Some(RenderResourceType::Buffer)
}
fn buffer_byte_len(&self) -> Option<usize> {
Some(12)
}
fn write_buffer_bytes(&self, buffer: &mut [u8]) {
// Write the size buffer
let (size_buf, flip_buf) = buffer.split_at_mut(8);
self.size.write_bytes(size_buf);
// First bit means flip x, second bit means flip y
flip_buf[0] = if self.flip_x { 0b01 } else { 0 } | if self.flip_y { 0b10 } else { 0 };
flip_buf[1] = 0;
flip_buf[2] = 0;
flip_buf[3] = 0;
}
fn texture(&self) -> Option<&Handle<Texture>> {
None
}
}
/// Determines how `Sprite` resize should be handled
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Reflect)]
#[reflect_value(PartialEq, Serialize, Deserialize)]
@ -34,6 +67,8 @@ impl Sprite {
Self {
size,
resize_mode: SpriteResizeMode::Manual,
flip_x: false,
flip_y: false,
}
}
}

View file

@ -1,11 +1,11 @@
use crate::Rect;
use bevy_asset::Handle;
use bevy_core::Byteable;
use bevy_core::Bytes;
use bevy_math::Vec2;
use bevy_reflect::TypeUuid;
use bevy_render::{
color::Color,
renderer::{RenderResource, RenderResources},
renderer::{RenderResource, RenderResourceType, RenderResources},
texture::Texture,
};
use bevy_utils::HashMap;
@ -25,11 +25,44 @@ pub struct TextureAtlas {
pub texture_handles: Option<HashMap<Handle<Texture>, usize>>,
}
#[derive(Debug, RenderResources, RenderResource, Clone)]
#[derive(Debug, Clone, RenderResources)]
#[render_resources(from_self)]
#[repr(C)]
pub struct TextureAtlasSprite {
pub color: Color,
pub index: u32,
pub flip_x: bool,
pub flip_y: bool,
}
impl RenderResource for TextureAtlasSprite {
fn resource_type(&self) -> Option<RenderResourceType> {
Some(RenderResourceType::Buffer)
}
fn buffer_byte_len(&self) -> Option<usize> {
Some(24)
}
fn write_buffer_bytes(&self, buffer: &mut [u8]) {
// Write the color buffer
let (color_buf, rest) = buffer.split_at_mut(16);
self.color.write_bytes(color_buf);
// Write the index buffer
let (index_buf, flip_buf) = rest.split_at_mut(4);
self.index.write_bytes(index_buf);
// First bit means flip x, second bit means flip y
flip_buf[0] = if self.flip_x { 0b01 } else { 0 } | if self.flip_y { 0b10 } else { 0 };
flip_buf[1] = 0;
flip_buf[2] = 0;
flip_buf[3] = 0;
}
fn texture(&self) -> Option<&Handle<Texture>> {
None
}
}
impl Default for TextureAtlasSprite {
@ -37,12 +70,12 @@ impl Default for TextureAtlasSprite {
Self {
index: 0,
color: Color::WHITE,
flip_x: false,
flip_y: false,
}
}
}
unsafe impl Byteable for TextureAtlasSprite {}
impl TextureAtlasSprite {
pub fn new(index: u32) -> TextureAtlasSprite {
Self {

View file

@ -72,6 +72,8 @@ impl<'a> Drawable for DrawableText<'a> {
let sprite = TextureAtlasSprite {
index: tv.atlas_info.glyph_index,
color: self.sections[tv.section_index].style.color,
flip_x: false,
flip_y: false,
};
// To get the rendering right for non-one scaling factors, we need

View file

@ -87,6 +87,7 @@ fn setup(
sprite: Sprite {
size: Vec2::new(1.0, 1.0) * SPRITE_SIZE,
resize_mode: SpriteResizeMode::Manual,
..Default::default()
},
material: materials.add(ColorMaterial {
color: COL_DESELECTED * col,

View file

@ -0,0 +1,29 @@
use bevy::prelude::*;
fn main() {
App::build()
.add_plugins(DefaultPlugins)
.add_startup_system(setup.system())
.run();
}
fn setup(
commands: &mut Commands,
asset_server: Res<AssetServer>,
mut materials: ResMut<Assets<ColorMaterial>>,
) {
let texture_handle = asset_server.load("branding/icon.png");
commands
.spawn(OrthographicCameraBundle::new_2d())
.spawn(SpriteBundle {
material: materials.add(texture_handle.into()),
sprite: Sprite {
// Flip the logo to the left
flip_x: true,
// And don't flip it upside-down ( the default )
flip_y: false,
..Default::default()
},
..Default::default()
});
}

View file

@ -68,6 +68,7 @@ Example | Main | Description
`sprite` | [`2d/sprite.rs`](./2d/sprite.rs) | Renders a sprite
`sprite_sheet` | [`2d/sprite_sheet.rs`](./2d/sprite_sheet.rs) | Renders an animated sprite
`text2d` | [`2d/text2d.rs`](./2d/text2d.rs) | Generates text in 2d
`sprite_flipping` | [`2d/sprite_flipping.rs`](./2d/sprite_flipping.rs) | Renders a sprite flipped along an axis
`texture_atlas` | [`2d/texture_atlas.rs`](./2d/texture_atlas.rs) | Generates a texture atlas (sprite sheet) from individual sprites
## 3D Rendering