//! 2d testbed //! //! You can switch scene by pressing the spacebar mod helpers; use argh::FromArgs; use bevy::prelude::*; use helpers::Next; #[derive(FromArgs)] /// 2d testbed pub struct Args { #[argh(positional)] scene: Option, } fn main() { #[cfg(not(target_arch = "wasm32"))] let args: Args = argh::from_env(); #[cfg(target_arch = "wasm32")] let args: Args = Args::from_args(&[], &[]).unwrap(); let mut app = App::new(); app.add_plugins((DefaultPlugins,)) .add_systems(OnEnter(Scene::Shapes), shapes::setup) .add_systems(OnEnter(Scene::Bloom), bloom::setup) .add_systems(OnEnter(Scene::Text), text::setup) .add_systems(OnEnter(Scene::Sprite), sprite::setup) .add_systems(OnEnter(Scene::SpriteSlicing), sprite_slicing::setup) .add_systems(OnEnter(Scene::Gizmos), gizmos::setup) .add_systems( OnEnter(Scene::TextureAtlasBuilder), texture_atlas_builder::setup, ) .add_systems(OnEnter(Scene::ColorConsistency), color_consistency::setup) .add_systems(OnExit(Scene::ColorConsistency), color_consistency::teardown) .add_systems(Update, switch_scene) .add_systems(Update, gizmos::draw_gizmos.run_if(in_state(Scene::Gizmos))); match args.scene { None => app.init_state::(), Some(scene) => app.insert_state(scene), }; #[cfg(feature = "bevy_ci_testing")] app.add_systems(Update, helpers::switch_scene_in_ci::); app.run(); } #[derive(Debug, Clone, Eq, PartialEq, Hash, States, Default)] enum Scene { #[default] Shapes, Bloom, Text, Sprite, SpriteSlicing, Gizmos, TextureAtlasBuilder, ColorConsistency, } impl std::str::FromStr for Scene { type Err = String; fn from_str(s: &str) -> std::result::Result { let mut isit = Self::default(); while s.to_lowercase() != format!("{isit:?}").to_lowercase() { isit = isit.next(); if isit == Self::default() { return Err(format!("Invalid Scene name: {s}")); } } Ok(isit) } } impl Next for Scene { fn next(&self) -> Self { match self { Scene::Shapes => Scene::Bloom, Scene::Bloom => Scene::Text, Scene::Text => Scene::Sprite, Scene::Sprite => Scene::SpriteSlicing, Scene::SpriteSlicing => Scene::Gizmos, Scene::Gizmos => Scene::TextureAtlasBuilder, Scene::TextureAtlasBuilder => Scene::ColorConsistency, Scene::ColorConsistency => Scene::Shapes, } } } fn switch_scene( keyboard: Res>, scene: Res>, mut next_scene: ResMut>, ) { if keyboard.just_pressed(KeyCode::Space) { info!("Switching scene"); next_scene.set(scene.get().next()); } } mod shapes { use bevy::prelude::*; const X_EXTENT: f32 = 900.; pub fn setup( mut commands: Commands, mut meshes: ResMut>, mut materials: ResMut>, ) { commands.spawn((Camera2d, DespawnOnExit(super::Scene::Shapes))); let shapes = [ meshes.add(Circle::new(50.0)), meshes.add(CircularSector::new(50.0, 1.0)), meshes.add(CircularSegment::new(50.0, 1.25)), meshes.add(Ellipse::new(25.0, 50.0)), meshes.add(Annulus::new(25.0, 50.0)), meshes.add(Capsule2d::new(25.0, 50.0)), meshes.add(Rhombus::new(75.0, 100.0)), meshes.add(Rectangle::new(50.0, 100.0)), meshes.add(RegularPolygon::new(50.0, 6)), meshes.add(Triangle2d::new( Vec2::Y * 50.0, Vec2::new(-50.0, -50.0), Vec2::new(50.0, -50.0), )), ]; let num_shapes = shapes.len(); for (i, shape) in shapes.into_iter().enumerate() { // Distribute colors evenly across the rainbow. let color = Color::hsl(360. * i as f32 / num_shapes as f32, 0.95, 0.7); commands.spawn(( Mesh2d(shape), MeshMaterial2d(materials.add(color)), Transform::from_xyz( // Distribute shapes from -X_EXTENT/2 to +X_EXTENT/2. -X_EXTENT / 2. + i as f32 / (num_shapes - 1) as f32 * X_EXTENT, 0.0, 0.0, ), DespawnOnExit(super::Scene::Shapes), )); } } } mod bloom { use bevy::{core_pipeline::tonemapping::Tonemapping, post_process::bloom::Bloom, prelude::*}; pub fn setup( mut commands: Commands, mut meshes: ResMut>, mut materials: ResMut>, ) { commands.spawn(( Camera2d, Tonemapping::TonyMcMapface, Bloom::default(), DespawnOnExit(super::Scene::Bloom), )); commands.spawn(( Mesh2d(meshes.add(Circle::new(100.))), MeshMaterial2d(materials.add(Color::srgb(7.5, 0.0, 7.5))), Transform::from_translation(Vec3::new(-200., 0., 0.)), DespawnOnExit(super::Scene::Bloom), )); commands.spawn(( Mesh2d(meshes.add(RegularPolygon::new(100., 6))), MeshMaterial2d(materials.add(Color::srgb(6.25, 9.4, 9.1))), Transform::from_translation(Vec3::new(200., 0., 0.)), DespawnOnExit(super::Scene::Bloom), )); } } mod text { use bevy::color::palettes; use bevy::prelude::*; use bevy::sprite::Anchor; use bevy::text::TextBounds; pub fn setup(mut commands: Commands, asset_server: Res) { commands.spawn((Camera2d, DespawnOnExit(super::Scene::Text))); for (i, justify) in [ Justify::Left, Justify::Right, Justify::Center, Justify::Justified, ] .into_iter() .enumerate() { let y = 230. - 150. * i as f32; spawn_anchored_text(&mut commands, -300. * Vec3::X + y * Vec3::Y, justify, None); spawn_anchored_text( &mut commands, 300. * Vec3::X + y * Vec3::Y, justify, Some(TextBounds::new(150., 60.)), ); } let sans_serif = TextFont::from(asset_server.load("fonts/FiraSans-Bold.ttf")); const NUM_ITERATIONS: usize = 10; for i in 0..NUM_ITERATIONS { let fraction = i as f32 / (NUM_ITERATIONS - 1) as f32; commands.spawn(( Text2d::new("Bevy"), sans_serif.clone(), Transform::from_xyz(0.0, fraction * 200.0, i as f32) .with_scale(1.0 + Vec2::splat(fraction).extend(1.)) .with_rotation(Quat::from_rotation_z(fraction * core::f32::consts::PI)), TextColor(Color::hsla(fraction * 360.0, 0.8, 0.8, 0.8)), DespawnOnExit(super::Scene::Text), )); } commands.spawn(( Text2d::new("This text is invisible."), Visibility::Hidden, DespawnOnExit(super::Scene::Text), )); } fn spawn_anchored_text( commands: &mut Commands, dest: Vec3, justify: Justify, bounds: Option, ) { commands.spawn(( Sprite { color: palettes::css::YELLOW.into(), custom_size: Some(5. * Vec2::ONE), ..Default::default() }, Transform::from_translation(dest), DespawnOnExit(super::Scene::Text), )); for anchor in [ Anchor::TOP_LEFT, Anchor::TOP_RIGHT, Anchor::BOTTOM_RIGHT, Anchor::BOTTOM_LEFT, ] { let mut text = commands.spawn(( Text2d::new("L R\n"), TextLayout::justify(justify), Transform::from_translation(dest + Vec3::Z), anchor, DespawnOnExit(super::Scene::Text), ShowAabbGizmo { color: Some(palettes::tailwind::AMBER_400.into()), }, children![ ( TextSpan::new(format!("{}, {}\n", anchor.x, anchor.y)), TextFont::from_font_size(14.0), TextColor(palettes::tailwind::BLUE_400.into()), ), ( TextSpan::new(format!("{justify:?}")), TextFont::from_font_size(14.0), TextColor(palettes::tailwind::GREEN_400.into()), ), ], )); if let Some(bounds) = bounds { text.insert(bounds); commands.spawn(( Sprite { color: palettes::tailwind::GRAY_900.into(), custom_size: Some(Vec2::new(bounds.width.unwrap(), bounds.height.unwrap())), ..Default::default() }, Transform::from_translation(dest - Vec3::Z), anchor, DespawnOnExit(super::Scene::Text), )); } } } } mod sprite { use bevy::color::palettes::css::{BLUE, LIME, RED}; use bevy::prelude::*; use bevy::sprite::Anchor; pub fn setup(mut commands: Commands, asset_server: Res) { commands.spawn((Camera2d, DespawnOnExit(super::Scene::Sprite))); for (anchor, flip_x, flip_y, color) in [ (Anchor::BOTTOM_LEFT, false, false, Color::WHITE), (Anchor::BOTTOM_RIGHT, true, false, RED.into()), (Anchor::TOP_LEFT, false, true, LIME.into()), (Anchor::TOP_RIGHT, true, true, BLUE.into()), ] { commands.spawn(( Sprite { image: asset_server.load("branding/bevy_logo_dark.png"), flip_x, flip_y, color, ..default() }, anchor, DespawnOnExit(super::Scene::Sprite), )); } } } mod sprite_slicing { use bevy::prelude::*; use bevy::sprite::{BorderRect, SliceScaleMode, SpriteImageMode, TextureSlicer}; pub fn setup(mut commands: Commands, asset_server: Res) { commands.spawn((Camera2d, DespawnOnExit(super::Scene::SpriteSlicing))); let texture = asset_server.load("textures/slice_square_2.png"); let font = asset_server.load("fonts/FiraSans-Bold.ttf"); commands.spawn(( Sprite { image: texture.clone(), ..default() }, Transform::from_translation(Vec3::new(-150.0, 50.0, 0.0)).with_scale(Vec3::splat(2.0)), DespawnOnExit(super::Scene::SpriteSlicing), )); commands.spawn(( Sprite { image: texture, image_mode: SpriteImageMode::Sliced(TextureSlicer { border: BorderRect::all(20.0), center_scale_mode: SliceScaleMode::Stretch, ..default() }), custom_size: Some(Vec2::new(200.0, 200.0)), ..default() }, Transform::from_translation(Vec3::new(150.0, 50.0, 0.0)), DespawnOnExit(super::Scene::SpriteSlicing), )); commands.spawn(( Text2d::new("Original"), TextFont { font: FontSource::from(font.clone()), font_size: FontSize::Px(20.0), ..default() }, Transform::from_translation(Vec3::new(-150.0, -80.0, 0.0)), DespawnOnExit(super::Scene::SpriteSlicing), )); commands.spawn(( Text2d::new("Sliced"), TextFont { font: FontSource::from(font.clone()), font_size: FontSize::Px(20.0), ..default() }, Transform::from_translation(Vec3::new(150.0, -80.0, 0.0)), DespawnOnExit(super::Scene::SpriteSlicing), )); } } mod gizmos { use bevy::{color::palettes::css::*, prelude::*}; pub fn setup(mut commands: Commands) { commands.spawn((Camera2d, DespawnOnExit(super::Scene::Gizmos))); } pub fn draw_gizmos(mut gizmos: Gizmos) { gizmos.rect_2d( Isometry2d::from_translation(Vec2::new(-200.0, 0.0)), Vec2::new(200.0, 200.0), RED, ); gizmos .circle_2d( Isometry2d::from_translation(Vec2::new(-200.0, 0.0)), 200.0, GREEN, ) .resolution(64); gizmos.text_2d( Isometry2d::from_translation(Vec2::new(-200.0, 0.0)), "text_2d gizmo", 15., Vec2 { x: 0., y: 0. }, Color::WHITE, ); // 2d grids with all variations of outer edges on or off for i in 0..4 { let x = 200.0 * (1.0 + (i % 2) as f32); let y = 150.0 * (0.5 - (i / 2) as f32); let mut grid = gizmos.grid( Vec3::new(x, y, 0.0), UVec2::new(5, 4), Vec2::splat(30.), Color::WHITE, ); if i & 1 > 0 { grid = grid.outer_edges_x(); } if i & 2 > 0 { grid.outer_edges_y(); } } } } mod texture_atlas_builder { use bevy::{ asset::RenderAssetUsages, image::ImageSampler, prelude::*, render::render_resource::{Extent3d, TextureDimension, TextureFormat}, sprite::Anchor, }; const ATLAS_SIZE: UVec2 = UVec2::splat(64); const IMAGE_SIZE: UVec2 = UVec2::splat(28); const PADDING_SIZE: UVec2 = UVec2::splat(2); const ATLAS_SCALE: f32 = 4.; const IMAGE_SCALE: f32 = 4.; pub fn setup( mut commands: Commands, mut textures: ResMut>, mut texture_atlases: ResMut>, ) { commands.spawn((Camera2d, DespawnOnExit(super::Scene::TextureAtlasBuilder))); for (i, padding) in [UVec2::ZERO, PADDING_SIZE].into_iter().enumerate() { // generate solid red green and blue and yellow images let images = [ [255, 0, 0, 255], [0, 255, 0, 255], [0, 0, 255, 255], [255, 255, 0, 255], ] .map(|pixel| { Image::new_fill( Extent3d { width: 28, height: 28, depth_or_array_layers: 1, }, TextureDimension::D2, &pixel, TextureFormat::Rgba8UnormSrgb, RenderAssetUsages::MAIN_WORLD | RenderAssetUsages::RENDER_WORLD, ) }); let mut texture_atlas_builder = TextureAtlasBuilder::default(); texture_atlas_builder .initial_size(ATLAS_SIZE) .max_size(ATLAS_SIZE) .padding(padding); for image in &images { texture_atlas_builder.add_texture(None, image); } let (atlas_layout, _, atlas_texture) = texture_atlas_builder.build().expect( "The images are 28 pixels square, so they should fit with 4 pixels left over", ); let atlas_layout = texture_atlases.add(atlas_layout); let mut nearest_atlas_image = atlas_texture.clone(); nearest_atlas_image.sampler = ImageSampler::nearest(); let atlas_handle = textures.add(atlas_texture); let nearest_atlas_handle = textures.add(nearest_atlas_image); let position = ((2. * i as f32 - 1.) * (0.625 * ATLAS_SIZE.x as f32 * ATLAS_SCALE)) .round() * Vec3::X; commands.spawn(( Sprite { image: nearest_atlas_handle, custom_size: Some(ATLAS_SIZE.as_vec2() * ATLAS_SCALE), ..default() }, Anchor::BOTTOM_CENTER, ShowAabbGizmo { color: Some(Color::WHITE), }, DespawnOnExit(super::Scene::TextureAtlasBuilder), Transform::from_translation(position), )); for (index, anchor) in [ Anchor::BOTTOM_RIGHT, Anchor::BOTTOM_LEFT, Anchor::TOP_LEFT, Anchor::TOP_RIGHT, ] .into_iter() .enumerate() { commands.spawn(( Sprite { image: atlas_handle.clone(), texture_atlas: Some(TextureAtlas { layout: atlas_layout.clone(), index, }), custom_size: Some(IMAGE_SIZE.as_vec2() * IMAGE_SCALE), ..default() }, Transform::from_translation( position + -2. * IMAGE_SCALE * (Vec3::Y * IMAGE_SIZE.y as f32 + anchor.as_vec().extend(0.)), ), anchor, DespawnOnExit(super::Scene::TextureAtlasBuilder), )); } } } } mod color_consistency { //! Visual regression test for . //! //! The clear color and shapes rendered using different pipelines (sprites, //! 2D meshes, UI background) should produce identical pixel values for the //! same sRGB input color. //! //! If the color conversion paths are consistent, the entire window will appear //! as a uniform solid color with no visible boundaries between the strips. use bevy::{core_pipeline::tonemapping::Tonemapping, prelude::*}; // We've chosen the sRGB value from issue #23577, in case it can be reproduced elsewhere. const TEST_COLOR: Color = Color::srgb(0.1, 0.1, 0.1); const DEFAULT_WIDTH: f32 = 1280.0; const DEFAULT_HEIGHT: f32 = 720.0; const STRIP_WIDTH: f32 = DEFAULT_WIDTH / 3.0; const STRIP_HEIGHT: f32 = DEFAULT_HEIGHT / 3.0; pub fn setup( mut commands: Commands, mut meshes: ResMut>, mut materials: ResMut>, ) { // The window background is drawn with the clear color. commands.insert_resource(ClearColor(TEST_COLOR)); // Make sure there's no tonemapping commands.spawn(( Camera2d, Tonemapping::None, DespawnOnExit(super::Scene::ColorConsistency), )); // Top third for sprites commands.spawn(( Sprite { color: TEST_COLOR, custom_size: Some(Vec2::new(STRIP_WIDTH, STRIP_HEIGHT)), ..default() }, Transform::from_xyz(0.0, STRIP_HEIGHT, 0.0), DespawnOnExit(super::Scene::ColorConsistency), )); // Middle third for 2D meshes commands.spawn(( Mesh2d(meshes.add(Rectangle::new(STRIP_WIDTH, STRIP_HEIGHT))), MeshMaterial2d(materials.add(ColorMaterial::from_color(TEST_COLOR))), Transform::from_xyz(0.0, 0.0, 0.0), DespawnOnExit(super::Scene::ColorConsistency), )); // Bottom third for UI nodes commands.spawn(( Node { position_type: PositionType::Absolute, bottom: Val::Px(0.0), left: Val::Px(0.0), width: Val::Percent(33.3), height: Val::Px(STRIP_HEIGHT), ..default() }, BackgroundColor(TEST_COLOR), DespawnOnExit(super::Scene::ColorConsistency), )); } // Remember to reset the clear color // Tonemapping is per-camera, and is reset when the camera despawns pub fn teardown(mut commands: Commands) { commands.insert_resource(ClearColor::default()); } }