Background Materials
It would be nice to have a background for the pipes.
The absolutely most straightforward way to put a background on screen is to spawn a Sprite. Here I’ve used the AssetServer to load the background_color_grass.png that was in the template’s assets folder. The background image is a 1024x1024 square from a Kenney asset pack, so I set the size of the image to be the width and height of our pre-defined CANVAS_SIZE.x.
commands.spawn((
Sprite {
image: asset_server
.load("background_color_grass.png"),
custom_size: Some(Vec2::splat(
CANVAS_SIZE.x,
)),
..default()
},
Transform::default(),
));
There’s one big problem… The bird is barely visible against our nice new background!
We can fix this by adjusting the color of the bird’s Sprite. This code parses a hex into an Srgba, which could fail so we unwrap, and then uses into to turn it into the Color the Sprite::color field expects. This is possible because both types are well known, Srgba and Color, and as it turns out all Color does is wrap Srgba!
commands.spawn((
Player,
Sprite {
custom_size: Some(Vec2::splat(PLAYER_SIZE)),
image: asset_server.load("bevy-bird.png"),
color: Srgba::hex("#282828").unwrap().into(),
..default()
},
Transform::from_xyz(-CANVAS_SIZE.x / 4.0, 0.0, 1.0),
));
An Animated Background
That looks much better but if you want to dig into shaders we can go further without much more effort.
We’ll need a few new items in scope; Specifically ImageAddressMode, AsBindGroup, ShaderRef, ImageLoaderSettings, Material2d, and Material2dPlugin. Here’s our new use section:
use bevy::{
camera::ScalingMode,
image::ImageAddressMode,
math::bounding::{
Aabb2d, BoundingCircle, IntersectsVolume,
},
prelude::*,
render::render_resource::AsBindGroup,
shader::ShaderRef,
sprite_render::{Material2d, Material2dPlugin},
};
use bevy::{
color::palettes::tailwind::{RED_400, SLATE_50},
image::ImageLoaderSettings,
};
use flappy_bird::*;
Firstly we’ll create a new struct with a few derives on it. The tldr; here is that we’re defining a new type of Asset that will be used to define the data that gets passed to our shader. Then, we implement Material2d for that type, which defines what shader will be executed.
#[derive(Asset, TypePath, AsBindGroup, Debug, Clone)]
pub struct BackgroundMaterial {
#[texture(0)]
#[sampler(1)]
pub color_texture: Handle<Image>,
}
impl Material2d for BackgroundMaterial {
fn fragment_shader() -> ShaderRef {
"background.wgsl".into()
}
}
We’ll write the wgsl file last here.
We need to add the Material2dPlugin to our app to set up the new material.
.add_plugins((
PipePlugin,
Material2dPlugin::<BackgroundMaterial>::default(
),
))
and in startup, add two parameters that request the Mesh assets and the BackgroundMaterial assets databases.
fn startup(
mut commands: Commands,
asset_server: Res<AssetServer>,
mut config_store: ResMut<GizmoConfigStore>,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<BackgroundMaterial>>,
) { ... }
We replace our Sprite background with a Mesh2d and the new material we defined. We also set the sampler to repeat the texture if we sample a coordinate outside of the texture’s bounds, which will be important in a minute when we write our wgsl file.
commands.spawn((
Mesh2d(meshes.add(Rectangle::new(
CANVAS_SIZE.x,
CANVAS_SIZE.x,
))),
MeshMaterial2d(materials.add(BackgroundMaterial {
color_texture: asset_server.load_with_settings(
"background_color_grass.png",
|settings: &mut ImageLoaderSettings| {
settings
.sampler
.get_or_init_descriptor()
.set_address_mode(
ImageAddressMode::Repeat,
);
},
),
})),
));
background wgsl
The background.wgsl file gets created in our assets directory. We define a couple of imports which notable bring in the VertexOutput and globals. We are writing a fragment shader, so we receive the data from the vertex shader that ran before us as well as the data we defined in our struct. globals is where we get the time, which allows us to scroll the background texture over time. We then have two bindings. The group and binding numbers are literally indices in a Vec. If you get deeper into writing shaders you will end up seeing (and writing!) the literal Vec these are defined in.
One of the bindings is the background image we loaded, and the other is the sampler, which defines how we access the texture. Remember that nearest configuration we used earlier in the workshop for our pixel art? That’s literally defining how this sampler behaves when we sample pixels from the texture.
Finally we actually execute a texture sample! We pass in the texture we loaded, the sampler, and a set of coordinates which pick the location in the texture to sample from. This is where the magic happens, we use the uvs of our Mesh2d, which are 0-1 values from edge to edge, and we move those coordinates over time, using a time value scaled down by 10. This results in the 0-1 value that we’re using to sample from the texture increasing each frame!
We configured the sampler to repeat if it went above 0-1, so the pixels of our texture will repeat from 1-2, 2-3, and so on.
#import bevy_sprite::mesh2d_vertex_output::VertexOutput
#import bevy_sprite::mesh2d_view_bindings::globals
@group(#{MATERIAL_BIND_GROUP}) @binding(0) var base_color_texture: texture_2d<f32>;
@group(#{MATERIAL_BIND_GROUP}) @binding(1) var base_color_sampler: sampler;
@fragment
fn fragment(mesh: VertexOutput) -> @location(0) vec4<f32> {
return textureSample(base_color_texture, base_color_sampler, mesh.uv + vec2(globals.time / 10., 0.));
}
and that’s it, you now have a scrolling background images that gives a parallax effect compared to the pipes!
Let’s also change the score text to the Bevy dark color while we’re here.
TextColor(Srgba::hex("#282828").unwrap().into()),
ScoreText,