Skip to main content

testbed_2d/
2d.rs

1//! 2d testbed
2//!
3//! You can switch scene by pressing the spacebar
4
5mod helpers;
6
7use argh::FromArgs;
8use bevy::prelude::*;
9
10use helpers::Next;
11
12#[derive(FromArgs)]
13/// 2d testbed
14pub struct Args {
15    #[argh(positional)]
16    scene: Option<Scene>,
17}
18
19fn main() {
20    #[cfg(not(target_arch = "wasm32"))]
21    let args: Args = argh::from_env();
22    #[cfg(target_arch = "wasm32")]
23    let args: Args = Args::from_args(&[], &[]).unwrap();
24
25    let mut app = App::new();
26    app.add_plugins((DefaultPlugins,))
27        .add_systems(OnEnter(Scene::Shapes), shapes::setup)
28        .add_systems(OnEnter(Scene::Bloom), bloom::setup)
29        .add_systems(OnEnter(Scene::Text), text::setup)
30        .add_systems(OnEnter(Scene::Sprite), sprite::setup)
31        .add_systems(OnEnter(Scene::SpriteSlicing), sprite_slicing::setup)
32        .add_systems(OnEnter(Scene::Gizmos), gizmos::setup)
33        .add_systems(
34            OnEnter(Scene::TextureAtlasBuilder),
35            texture_atlas_builder::setup,
36        )
37        .add_systems(OnEnter(Scene::ColorConsistency), color_consistency::setup)
38        .add_systems(OnExit(Scene::ColorConsistency), color_consistency::teardown)
39        .add_systems(Update, switch_scene)
40        .add_systems(Update, gizmos::draw_gizmos.run_if(in_state(Scene::Gizmos)));
41
42    match args.scene {
43        None => app.init_state::<Scene>(),
44        Some(scene) => app.insert_state(scene),
45    };
46
47    #[cfg(feature = "bevy_ci_testing")]
48    app.add_systems(Update, helpers::switch_scene_in_ci::<Scene>);
49
50    app.run();
51}
52
53#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, States, Default)]
54enum Scene {
55    #[default]
56    Shapes,
57    Bloom,
58    Text,
59    Sprite,
60    SpriteSlicing,
61    Gizmos,
62    TextureAtlasBuilder,
63    ColorConsistency,
64}
65
66impl Scene {
67    const ALL_ORDERED: &'static [Scene] = &[
68        Scene::Shapes,
69        Scene::Bloom,
70        Scene::Text,
71        Scene::Sprite,
72        Scene::SpriteSlicing,
73        Scene::Gizmos,
74        Scene::TextureAtlasBuilder,
75        Scene::ColorConsistency,
76    ];
77}
78
79impl std::str::FromStr for Scene {
80    type Err = String;
81
82    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
83        let mut isit = Self::default();
84        while s.to_lowercase() != format!("{isit:?}").to_lowercase() {
85            isit = isit.next();
86            if isit == Self::default() {
87                return Err(format!("Invalid Scene name: {s}"));
88            }
89        }
90        Ok(isit)
91    }
92}
93
94impl Next for Scene {
95    fn next(&self) -> Self {
96        Scene::ALL_ORDERED[(Scene::ALL_ORDERED
97            .iter()
98            .position(|scene| scene == self)
99            .unwrap()
100            + 1)
101            % Scene::ALL_ORDERED.len()]
102    }
103}
104
105fn switch_scene(
106    keyboard: Res<ButtonInput<KeyCode>>,
107    scene: Res<State<Scene>>,
108    mut next_scene: ResMut<NextState<Scene>>,
109) {
110    if keyboard.just_pressed(KeyCode::Space) {
111        info!("Switching scene");
112        next_scene.set(scene.get().next());
113    }
114}
115
116mod shapes {
117    use bevy::prelude::*;
118
119    const X_EXTENT: f32 = 900.;
120
121    pub fn setup(
122        mut commands: Commands,
123        mut meshes: ResMut<Assets<Mesh>>,
124        mut materials: ResMut<Assets<ColorMaterial>>,
125    ) {
126        commands.spawn((Camera2d, DespawnOnExit(super::Scene::Shapes)));
127
128        let shapes = [
129            meshes.add(Circle::new(50.0)),
130            meshes.add(CircularSector::new(50.0, 1.0)),
131            meshes.add(CircularSegment::new(50.0, 1.25)),
132            meshes.add(Ellipse::new(25.0, 50.0)),
133            meshes.add(Annulus::new(25.0, 50.0)),
134            meshes.add(Capsule2d::new(25.0, 50.0)),
135            meshes.add(Rhombus::new(75.0, 100.0)),
136            meshes.add(Rectangle::new(50.0, 100.0)),
137            meshes.add(RegularPolygon::new(50.0, 6)),
138            meshes.add(Triangle2d::new(
139                Vec2::Y * 50.0,
140                Vec2::new(-50.0, -50.0),
141                Vec2::new(50.0, -50.0),
142            )),
143        ];
144        let num_shapes = shapes.len();
145
146        for (i, shape) in shapes.into_iter().enumerate() {
147            // Distribute colors evenly across the rainbow.
148            let color = Color::hsl(360. * i as f32 / num_shapes as f32, 0.95, 0.7);
149
150            commands.spawn((
151                Mesh2d(shape),
152                MeshMaterial2d(materials.add(color)),
153                Transform::from_xyz(
154                    // Distribute shapes from -X_EXTENT/2 to +X_EXTENT/2.
155                    -X_EXTENT / 2. + i as f32 / (num_shapes - 1) as f32 * X_EXTENT,
156                    0.0,
157                    0.0,
158                ),
159                DespawnOnExit(super::Scene::Shapes),
160            ));
161        }
162    }
163}
164
165mod bloom {
166    use bevy::{core_pipeline::tonemapping::Tonemapping, post_process::bloom::Bloom, prelude::*};
167
168    pub fn setup(
169        mut commands: Commands,
170        mut meshes: ResMut<Assets<Mesh>>,
171        mut materials: ResMut<Assets<ColorMaterial>>,
172    ) {
173        commands.spawn((
174            Camera2d,
175            Tonemapping::TonyMcMapface,
176            Bloom::default(),
177            DespawnOnExit(super::Scene::Bloom),
178        ));
179
180        commands.spawn((
181            Mesh2d(meshes.add(Circle::new(100.))),
182            MeshMaterial2d(materials.add(Color::srgb(7.5, 0.0, 7.5))),
183            Transform::from_translation(Vec3::new(-200., 0., 0.)),
184            DespawnOnExit(super::Scene::Bloom),
185        ));
186
187        commands.spawn((
188            Mesh2d(meshes.add(RegularPolygon::new(100., 6))),
189            MeshMaterial2d(materials.add(Color::srgb(6.25, 9.4, 9.1))),
190            Transform::from_translation(Vec3::new(200., 0., 0.)),
191            DespawnOnExit(super::Scene::Bloom),
192        ));
193    }
194}
195
196mod text {
197    use bevy::color::palettes;
198    use bevy::prelude::*;
199    use bevy::sprite::Anchor;
200    use bevy::text::TextBounds;
201
202    pub fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
203        commands.spawn((Camera2d, DespawnOnExit(super::Scene::Text)));
204
205        for (i, justify) in [
206            Justify::Left,
207            Justify::Right,
208            Justify::Center,
209            Justify::Justified,
210        ]
211        .into_iter()
212        .enumerate()
213        {
214            let y = 230. - 150. * i as f32;
215            spawn_anchored_text(&mut commands, -300. * Vec3::X + y * Vec3::Y, justify, None);
216            spawn_anchored_text(
217                &mut commands,
218                300. * Vec3::X + y * Vec3::Y,
219                justify,
220                Some(TextBounds::new(150., 60.)),
221            );
222        }
223
224        let sans_serif = TextFont::from(asset_server.load("fonts/FiraSans-Bold.ttf"));
225
226        const NUM_ITERATIONS: usize = 10;
227        for i in 0..NUM_ITERATIONS {
228            let fraction = i as f32 / (NUM_ITERATIONS - 1) as f32;
229
230            commands.spawn((
231                Text2d::new("Bevy"),
232                sans_serif.clone(),
233                Transform::from_xyz(0.0, fraction * 200.0, i as f32)
234                    .with_scale(1.0 + Vec2::splat(fraction).extend(1.))
235                    .with_rotation(Quat::from_rotation_z(fraction * core::f32::consts::PI)),
236                TextColor(Color::hsla(fraction * 360.0, 0.8, 0.8, 0.8)),
237                DespawnOnExit(super::Scene::Text),
238            ));
239        }
240
241        commands.spawn((
242            Text2d::new("This text is invisible."),
243            Visibility::Hidden,
244            DespawnOnExit(super::Scene::Text),
245        ));
246    }
247
248    fn spawn_anchored_text(
249        commands: &mut Commands,
250        dest: Vec3,
251        justify: Justify,
252        bounds: Option<TextBounds>,
253    ) {
254        commands.spawn((
255            Sprite {
256                color: palettes::css::YELLOW.into(),
257                custom_size: Some(5. * Vec2::ONE),
258                ..Default::default()
259            },
260            Transform::from_translation(dest),
261            DespawnOnExit(super::Scene::Text),
262        ));
263
264        for anchor in [
265            Anchor::TOP_LEFT,
266            Anchor::TOP_RIGHT,
267            Anchor::BOTTOM_RIGHT,
268            Anchor::BOTTOM_LEFT,
269        ] {
270            let mut text = commands.spawn((
271                Text2d::new("L R\n"),
272                TextLayout::justify(justify),
273                Transform::from_translation(dest + Vec3::Z),
274                anchor,
275                DespawnOnExit(super::Scene::Text),
276                ShowAabbGizmo {
277                    color: Some(palettes::tailwind::AMBER_400.into()),
278                },
279                children![
280                    (
281                        TextSpan::new(format!("{}, {}\n", anchor.x, anchor.y)),
282                        TextFont::from_font_size(14.0),
283                        TextColor(palettes::tailwind::BLUE_400.into()),
284                    ),
285                    (
286                        TextSpan::new(format!("{justify:?}")),
287                        TextFont::from_font_size(14.0),
288                        TextColor(palettes::tailwind::GREEN_400.into()),
289                    ),
290                ],
291            ));
292            if let Some(bounds) = bounds {
293                text.insert(bounds);
294
295                commands.spawn((
296                    Sprite {
297                        color: palettes::tailwind::GRAY_900.into(),
298                        custom_size: Some(Vec2::new(bounds.width.unwrap(), bounds.height.unwrap())),
299                        ..Default::default()
300                    },
301                    Transform::from_translation(dest - Vec3::Z),
302                    anchor,
303                    DespawnOnExit(super::Scene::Text),
304                ));
305            }
306        }
307    }
308}
309
310mod sprite {
311    use bevy::color::palettes::css::{BLUE, LIME, RED};
312    use bevy::prelude::*;
313    use bevy::sprite::Anchor;
314
315    pub fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
316        commands.spawn((Camera2d, DespawnOnExit(super::Scene::Sprite)));
317        for (anchor, flip_x, flip_y, color) in [
318            (Anchor::BOTTOM_LEFT, false, false, Color::WHITE),
319            (Anchor::BOTTOM_RIGHT, true, false, RED.into()),
320            (Anchor::TOP_LEFT, false, true, LIME.into()),
321            (Anchor::TOP_RIGHT, true, true, BLUE.into()),
322        ] {
323            commands.spawn((
324                Sprite {
325                    image: asset_server.load("branding/bevy_logo_dark.png"),
326                    flip_x,
327                    flip_y,
328                    color,
329                    ..default()
330                },
331                anchor,
332                DespawnOnExit(super::Scene::Sprite),
333            ));
334        }
335    }
336}
337
338mod sprite_slicing {
339    use bevy::prelude::*;
340    use bevy::sprite::{BorderRect, SliceScaleMode, SpriteImageMode, TextureSlicer};
341
342    pub fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
343        commands.spawn((Camera2d, DespawnOnExit(super::Scene::SpriteSlicing)));
344
345        let texture = asset_server.load("textures/slice_square_2.png");
346        let font = asset_server.load("fonts/FiraSans-Bold.ttf");
347
348        commands.spawn((
349            Sprite {
350                image: texture.clone(),
351                ..default()
352            },
353            Transform::from_translation(Vec3::new(-150.0, 50.0, 0.0)).with_scale(Vec3::splat(2.0)),
354            DespawnOnExit(super::Scene::SpriteSlicing),
355        ));
356
357        commands.spawn((
358            Sprite {
359                image: texture,
360                image_mode: SpriteImageMode::Sliced(TextureSlicer {
361                    border: BorderRect::all(20.0),
362                    center_scale_mode: SliceScaleMode::Stretch,
363                    ..default()
364                }),
365                custom_size: Some(Vec2::new(200.0, 200.0)),
366                ..default()
367            },
368            Transform::from_translation(Vec3::new(150.0, 50.0, 1.0)),
369            DespawnOnExit(super::Scene::SpriteSlicing),
370        ));
371
372        commands.spawn((
373            Text2d::new("Original"),
374            TextFont {
375                font: FontSource::from(font.clone()),
376                font_size: FontSize::Px(20.0),
377                ..default()
378            },
379            Transform::from_translation(Vec3::new(-150.0, -80.0, 0.0)),
380            DespawnOnExit(super::Scene::SpriteSlicing),
381        ));
382
383        commands.spawn((
384            Text2d::new("Sliced"),
385            TextFont {
386                font: FontSource::from(font.clone()),
387                font_size: FontSize::Px(20.0),
388                ..default()
389            },
390            Transform::from_translation(Vec3::new(150.0, -80.0, 0.0)),
391            DespawnOnExit(super::Scene::SpriteSlicing),
392        ));
393    }
394}
395
396mod gizmos {
397    use bevy::{color::palettes::css::*, prelude::*};
398
399    pub fn setup(mut commands: Commands) {
400        commands.spawn((Camera2d, DespawnOnExit(super::Scene::Gizmos)));
401    }
402
403    pub fn draw_gizmos(mut gizmos: Gizmos) {
404        gizmos.rect_2d(
405            Isometry2d::from_translation(Vec2::new(-200.0, 0.0)),
406            Vec2::new(200.0, 200.0),
407            RED,
408        );
409        gizmos
410            .circle_2d(
411                Isometry2d::from_translation(Vec2::new(-200.0, 0.0)),
412                200.0,
413                GREEN,
414            )
415            .resolution(64);
416
417        gizmos.text_2d(
418            Isometry2d::from_translation(Vec2::new(-200.0, 0.0)),
419            "text_2d gizmo",
420            15.,
421            Vec2 { x: 0., y: 0. },
422            Color::WHITE,
423        );
424
425        // 2d grids with all variations of outer edges on or off
426        for i in 0..4 {
427            let x = 200.0 * (1.0 + (i % 2) as f32);
428            let y = 150.0 * (0.5 - (i / 2) as f32);
429            let mut grid = gizmos.grid(
430                Vec3::new(x, y, 0.0),
431                UVec2::new(5, 4),
432                Vec2::splat(30.),
433                Color::WHITE,
434            );
435            if i & 1 > 0 {
436                grid = grid.outer_edges_x();
437            }
438            if i & 2 > 0 {
439                grid.outer_edges_y();
440            }
441        }
442    }
443}
444
445mod texture_atlas_builder {
446    use bevy::{
447        asset::RenderAssetUsages,
448        image::ImageSampler,
449        prelude::*,
450        render::render_resource::{Extent3d, TextureDimension, TextureFormat},
451        sprite::Anchor,
452    };
453
454    const ATLAS_SIZE: UVec2 = UVec2::splat(64);
455    const IMAGE_SIZE: UVec2 = UVec2::splat(28);
456    const PADDING_SIZE: UVec2 = UVec2::splat(2);
457    const ATLAS_SCALE: f32 = 4.;
458    const IMAGE_SCALE: f32 = 4.;
459
460    pub fn setup(
461        mut commands: Commands,
462        mut textures: ResMut<Assets<Image>>,
463        mut texture_atlases: ResMut<Assets<TextureAtlasLayout>>,
464    ) {
465        commands.spawn((Camera2d, DespawnOnExit(super::Scene::TextureAtlasBuilder)));
466
467        for (i, padding) in [UVec2::ZERO, PADDING_SIZE].into_iter().enumerate() {
468            // generate solid red green and blue and yellow images
469            let images = [
470                [255, 0, 0, 255],
471                [0, 255, 0, 255],
472                [0, 0, 255, 255],
473                [255, 255, 0, 255],
474            ]
475            .map(|pixel| {
476                Image::new_fill(
477                    Extent3d {
478                        width: 28,
479                        height: 28,
480                        depth_or_array_layers: 1,
481                    },
482                    TextureDimension::D2,
483                    &pixel,
484                    TextureFormat::Rgba8UnormSrgb,
485                    RenderAssetUsages::MAIN_WORLD | RenderAssetUsages::RENDER_WORLD,
486                )
487            });
488
489            let mut texture_atlas_builder = TextureAtlasBuilder::default();
490            texture_atlas_builder
491                .initial_size(ATLAS_SIZE)
492                .max_size(ATLAS_SIZE)
493                .padding(padding);
494            for image in &images {
495                texture_atlas_builder.add_texture(None, image);
496            }
497
498            let (atlas_layout, _, atlas_texture) = texture_atlas_builder.build().expect(
499                "The images are 28 pixels square, so they should fit with 4 pixels left over",
500            );
501            let atlas_layout = texture_atlases.add(atlas_layout);
502
503            let mut nearest_atlas_image = atlas_texture.clone();
504            nearest_atlas_image.sampler = ImageSampler::nearest();
505
506            let atlas_handle = textures.add(atlas_texture);
507            let nearest_atlas_handle = textures.add(nearest_atlas_image);
508
509            let position = ((2. * i as f32 - 1.) * (0.625 * ATLAS_SIZE.x as f32 * ATLAS_SCALE))
510                .round()
511                * Vec3::X;
512
513            commands.spawn((
514                Sprite {
515                    image: nearest_atlas_handle,
516                    custom_size: Some(ATLAS_SIZE.as_vec2() * ATLAS_SCALE),
517                    ..default()
518                },
519                Anchor::BOTTOM_CENTER,
520                ShowAabbGizmo {
521                    color: Some(Color::WHITE),
522                },
523                DespawnOnExit(super::Scene::TextureAtlasBuilder),
524                Transform::from_translation(position),
525            ));
526
527            for (index, anchor) in [
528                Anchor::BOTTOM_RIGHT,
529                Anchor::BOTTOM_LEFT,
530                Anchor::TOP_LEFT,
531                Anchor::TOP_RIGHT,
532            ]
533            .into_iter()
534            .enumerate()
535            {
536                commands.spawn((
537                    Sprite {
538                        image: atlas_handle.clone(),
539                        texture_atlas: Some(TextureAtlas {
540                            layout: atlas_layout.clone(),
541                            index,
542                        }),
543                        custom_size: Some(IMAGE_SIZE.as_vec2() * IMAGE_SCALE),
544                        ..default()
545                    },
546                    Transform::from_translation(
547                        position
548                            + -2.
549                                * IMAGE_SCALE
550                                * (Vec3::Y * IMAGE_SIZE.y as f32 + anchor.as_vec().extend(0.)),
551                    ),
552                    anchor,
553                    DespawnOnExit(super::Scene::TextureAtlasBuilder),
554                ));
555            }
556        }
557    }
558}
559
560mod color_consistency {
561    //! Visual regression test for <https://github.com/bevyengine/bevy/issues/23577>.
562    //!
563    //! The clear color and shapes rendered using different pipelines (sprites,
564    //! 2D meshes, UI background) should produce identical pixel values for the
565    //! same sRGB input color.
566    //!
567    //! If the color conversion paths are consistent, the entire window will appear
568    //! as a uniform solid color with no visible boundaries between the strips.
569
570    use bevy::{core_pipeline::tonemapping::Tonemapping, prelude::*};
571
572    // We've chosen the sRGB value from issue #23577, in case it can be reproduced elsewhere.
573    const TEST_COLOR: Color = Color::srgb(0.1, 0.1, 0.1);
574    const DEFAULT_WIDTH: f32 = 1280.0;
575    const DEFAULT_HEIGHT: f32 = 720.0;
576    const STRIP_WIDTH: f32 = DEFAULT_WIDTH / 3.0;
577    const STRIP_HEIGHT: f32 = DEFAULT_HEIGHT / 3.0;
578
579    pub fn setup(
580        mut commands: Commands,
581        mut meshes: ResMut<Assets<Mesh>>,
582        mut materials: ResMut<Assets<ColorMaterial>>,
583    ) {
584        // The window background is drawn with the clear color.
585        commands.insert_resource(ClearColor(TEST_COLOR));
586
587        // Make sure there's no tonemapping
588        commands.spawn((
589            Camera2d,
590            Tonemapping::None,
591            DespawnOnExit(super::Scene::ColorConsistency),
592        ));
593
594        // Top third for sprites
595        commands.spawn((
596            Sprite {
597                color: TEST_COLOR,
598                custom_size: Some(Vec2::new(STRIP_WIDTH, STRIP_HEIGHT)),
599                ..default()
600            },
601            Transform::from_xyz(0.0, STRIP_HEIGHT, 0.0),
602            DespawnOnExit(super::Scene::ColorConsistency),
603        ));
604
605        // Middle third for 2D meshes
606        commands.spawn((
607            Mesh2d(meshes.add(Rectangle::new(STRIP_WIDTH, STRIP_HEIGHT))),
608            MeshMaterial2d(materials.add(ColorMaterial::from_color(TEST_COLOR))),
609            Transform::from_xyz(0.0, 0.0, 0.0),
610            DespawnOnExit(super::Scene::ColorConsistency),
611        ));
612
613        // Bottom third for UI nodes
614        commands.spawn((
615            Node {
616                position_type: PositionType::Absolute,
617                bottom: Val::Px(0.0),
618                left: Val::Px(0.0),
619                width: Val::Percent(33.3),
620                height: Val::Px(STRIP_HEIGHT),
621                ..default()
622            },
623            BackgroundColor(TEST_COLOR),
624            DespawnOnExit(super::Scene::ColorConsistency),
625        ));
626    }
627
628    // Remember to reset the clear color
629    // Tonemapping is per-camera, and is reset when the camera despawns
630    pub fn teardown(mut commands: Commands) {
631        commands.insert_resource(ClearColor::default());
632    }
633}