|
| 1 | +//! Shows how to create graphics that snap to the pixel grid by rendering to a texture in 2D |
| 2 | +
|
| 3 | +use bevy::{ |
| 4 | + prelude::*, |
| 5 | + render::{ |
| 6 | + camera::RenderTarget, |
| 7 | + render_resource::{ |
| 8 | + Extent3d, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages, |
| 9 | + }, |
| 10 | + view::RenderLayers, |
| 11 | + }, |
| 12 | + sprite::MaterialMesh2dBundle, |
| 13 | + window::WindowResized, |
| 14 | +}; |
| 15 | + |
| 16 | +/// In-game resolution width. |
| 17 | +const RES_WIDTH: u32 = 160; |
| 18 | + |
| 19 | +/// In-game resolution height. |
| 20 | +const RES_HEIGHT: u32 = 90; |
| 21 | + |
| 22 | +/// Default render layers for pixel-perfect rendering. |
| 23 | +/// You can skip adding this component, as this is the default. |
| 24 | +const PIXEL_PERFECT_LAYERS: RenderLayers = RenderLayers::layer(0); |
| 25 | + |
| 26 | +/// Render layers for high-resolution rendering. |
| 27 | +const HIGH_RES_LAYERS: RenderLayers = RenderLayers::layer(1); |
| 28 | + |
| 29 | +fn main() { |
| 30 | + App::new() |
| 31 | + .add_plugins(DefaultPlugins.set(ImagePlugin::default_nearest())) |
| 32 | + .insert_resource(Msaa::Off) |
| 33 | + .add_systems(Startup, (setup_camera, setup_sprite, setup_mesh)) |
| 34 | + .add_systems(Update, (rotate, fit_canvas)) |
| 35 | + .run(); |
| 36 | +} |
| 37 | + |
| 38 | +/// Low-resolution texture that contains the pixel-perfect world. |
| 39 | +/// Canvas itself is rendered to the high-resolution world. |
| 40 | +#[derive(Component)] |
| 41 | +struct Canvas; |
| 42 | + |
| 43 | +/// Camera that renders the pixel-perfect world to the [`Canvas`]. |
| 44 | +#[derive(Component)] |
| 45 | +struct InGameCamera; |
| 46 | + |
| 47 | +/// Camera that renders the [`Canvas`] (and other graphics on [`HIGH_RES_LAYERS`]) to the screen. |
| 48 | +#[derive(Component)] |
| 49 | +struct OuterCamera; |
| 50 | + |
| 51 | +#[derive(Component)] |
| 52 | +struct Rotate; |
| 53 | + |
| 54 | +fn setup_sprite(mut commands: Commands, asset_server: Res<AssetServer>) { |
| 55 | + // the sample sprite that will be rendered to the pixel-perfect canvas |
| 56 | + commands.spawn(( |
| 57 | + SpriteBundle { |
| 58 | + texture: asset_server.load("pixel/bevy_pixel_dark.png"), |
| 59 | + transform: Transform::from_xyz(-40., 20., 2.), |
| 60 | + ..default() |
| 61 | + }, |
| 62 | + Rotate, |
| 63 | + PIXEL_PERFECT_LAYERS, |
| 64 | + )); |
| 65 | + |
| 66 | + // the sample sprite that will be rendered to the high-res "outer world" |
| 67 | + commands.spawn(( |
| 68 | + SpriteBundle { |
| 69 | + texture: asset_server.load("pixel/bevy_pixel_light.png"), |
| 70 | + transform: Transform::from_xyz(-40., -20., 2.), |
| 71 | + ..default() |
| 72 | + }, |
| 73 | + Rotate, |
| 74 | + HIGH_RES_LAYERS, |
| 75 | + )); |
| 76 | +} |
| 77 | + |
| 78 | +/// Spawns a capsule mesh on the pixel-perfect layer. |
| 79 | +fn setup_mesh( |
| 80 | + mut commands: Commands, |
| 81 | + mut meshes: ResMut<Assets<Mesh>>, |
| 82 | + mut materials: ResMut<Assets<ColorMaterial>>, |
| 83 | +) { |
| 84 | + commands.spawn(( |
| 85 | + MaterialMesh2dBundle { |
| 86 | + mesh: meshes.add(Mesh::from(shape::Capsule::default())).into(), |
| 87 | + transform: Transform::from_xyz(40., 0., 2.).with_scale(Vec3::splat(32.)), |
| 88 | + material: materials.add(ColorMaterial::from(Color::BLACK)), |
| 89 | + ..default() |
| 90 | + }, |
| 91 | + Rotate, |
| 92 | + PIXEL_PERFECT_LAYERS, |
| 93 | + )); |
| 94 | +} |
| 95 | + |
| 96 | +fn setup_camera(mut commands: Commands, mut images: ResMut<Assets<Image>>) { |
| 97 | + let canvas_size = Extent3d { |
| 98 | + width: RES_WIDTH, |
| 99 | + height: RES_HEIGHT, |
| 100 | + ..default() |
| 101 | + }; |
| 102 | + |
| 103 | + // this Image serves as a canvas representing the low-resolution game screen |
| 104 | + let mut canvas = Image { |
| 105 | + texture_descriptor: TextureDescriptor { |
| 106 | + label: None, |
| 107 | + size: canvas_size, |
| 108 | + dimension: TextureDimension::D2, |
| 109 | + format: TextureFormat::Bgra8UnormSrgb, |
| 110 | + mip_level_count: 1, |
| 111 | + sample_count: 1, |
| 112 | + usage: TextureUsages::TEXTURE_BINDING |
| 113 | + | TextureUsages::COPY_DST |
| 114 | + | TextureUsages::RENDER_ATTACHMENT, |
| 115 | + view_formats: &[], |
| 116 | + }, |
| 117 | + ..default() |
| 118 | + }; |
| 119 | + |
| 120 | + // fill image.data with zeroes |
| 121 | + canvas.resize(canvas_size); |
| 122 | + |
| 123 | + let image_handle = images.add(canvas); |
| 124 | + |
| 125 | + // this camera renders whatever is on `PIXEL_PERFECT_LAYERS` to the canvas |
| 126 | + commands.spawn(( |
| 127 | + Camera2dBundle { |
| 128 | + camera: Camera { |
| 129 | + // render before the "main pass" camera |
| 130 | + order: -1, |
| 131 | + target: RenderTarget::Image(image_handle.clone()), |
| 132 | + ..default() |
| 133 | + }, |
| 134 | + ..default() |
| 135 | + }, |
| 136 | + InGameCamera, |
| 137 | + PIXEL_PERFECT_LAYERS, |
| 138 | + )); |
| 139 | + |
| 140 | + // spawn the canvas |
| 141 | + commands.spawn(( |
| 142 | + SpriteBundle { |
| 143 | + texture: image_handle, |
| 144 | + ..default() |
| 145 | + }, |
| 146 | + Canvas, |
| 147 | + HIGH_RES_LAYERS, |
| 148 | + )); |
| 149 | + |
| 150 | + // the "outer" camera renders whatever is on `HIGH_RES_LAYERS` to the screen. |
| 151 | + // here, the canvas and one of the sample sprites will be rendered by this camera |
| 152 | + commands.spawn((Camera2dBundle::default(), OuterCamera, HIGH_RES_LAYERS)); |
| 153 | +} |
| 154 | + |
| 155 | +/// Rotates entities to demonstrate grid snapping. |
| 156 | +fn rotate(time: Res<Time>, mut transforms: Query<&mut Transform, With<Rotate>>) { |
| 157 | + for mut transform in &mut transforms { |
| 158 | + let dt = time.delta_seconds(); |
| 159 | + transform.rotate_z(dt); |
| 160 | + } |
| 161 | +} |
| 162 | + |
| 163 | +/// Scales camera projection to fit the window (integer multiples only). |
| 164 | +fn fit_canvas( |
| 165 | + mut resize_events: EventReader<WindowResized>, |
| 166 | + mut projections: Query<&mut OrthographicProjection, With<OuterCamera>>, |
| 167 | +) { |
| 168 | + for event in resize_events.read() { |
| 169 | + let h_scale = event.width / RES_WIDTH as f32; |
| 170 | + let v_scale = event.height / RES_HEIGHT as f32; |
| 171 | + let mut projection = projections.single_mut(); |
| 172 | + projection.scale = 1. / h_scale.min(v_scale).round(); |
| 173 | + } |
| 174 | +} |
0 commit comments