Created
May 27, 2026 19:52
-
-
Save ForwardFeed/7c2b77097a80915c421ed85adc064358 to your computer and use it in GitHub Desktop.
Bevy Pixel Perfect Picking example fix
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| use bevy::{ | |
| asset::uuid::Uuid, camera::{ | |
| NormalizedRenderTarget, RenderTarget, visibility::RenderLayers | |
| }, color::palettes::css::GRAY, ecs::message::MessageCursor, picking::{ | |
| PickingSystems, pointer::{Location, PointerId, PointerInput} | |
| }, prelude::*, render::render_resource::{ | |
| Extent3d, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages, | |
| }, window::{PrimaryWindow, WindowResized} | |
| }; | |
| const RATIO_X: u32 = 16; | |
| const RATIO_Y: u32 = 9; | |
| const RATIO_MULTIPLICATOR: u32 = 10; | |
| const RES_WIDTH: u32 = RATIO_X * RATIO_MULTIPLICATOR; | |
| const RES_HEIGHT: u32 = RATIO_Y * RATIO_MULTIPLICATOR; | |
| /// Default render layers for pixel-perfect rendering. | |
| /// You can skip adding this component, as this is the default. | |
| const PIXEL_PERFECT_LAYERS: RenderLayers = RenderLayers::layer(0); | |
| /// Render layers for high-resolution rendering. | |
| const HIGH_RES_LAYERS: RenderLayers = RenderLayers::layer(1); | |
| fn main() { | |
| App::new() | |
| .add_plugins(DefaultPlugins.set(ImagePlugin::default_nearest())) | |
| .add_systems(Startup, (setup_camera, setup_sprite, setup_mesh)) | |
| .add_systems(Update, (rotate, fit_canvas)) | |
| .add_plugins(MeshPickingPlugin) | |
| .add_plugins(VirtualPointerForLowResCamera) | |
| .run(); | |
| } | |
| /// Low-resolution texture that contains the pixel-perfect world. | |
| /// Canvas itself is rendered to the high-resolution world. | |
| #[derive(Component)] | |
| struct Canvas; | |
| /// Camera that renders the pixel-perfect world to the [`Canvas`]. | |
| #[derive(Component)] | |
| pub struct InGameCamera; | |
| /// Camera that renders the [`Canvas`] (and other graphics on [`HIGH_RES_LAYERS`]) to the screen. | |
| #[derive(Component)] | |
| struct OuterCamera; | |
| #[derive(Component)] | |
| struct Rotate; | |
| fn setup_sprite(mut commands: Commands, asset_server: Res<AssetServer>) { | |
| // The sample sprite that will be rendered to the pixel-perfect canvas | |
| commands.spawn(( | |
| Sprite::from_image(asset_server.load("pixel/bevy_pixel_dark.png")), | |
| Transform::from_xyz(-45., 20., 2.), | |
| Rotate, | |
| Pickable::default(), | |
| PIXEL_PERFECT_LAYERS, | |
| )).observe(trigger_debug_text::<Pointer<Over>>("Dark".to_string())); | |
| // The sample sprite that will be rendered to the high-res "outer world" | |
| commands.spawn(( | |
| Sprite::from_image(asset_server.load("pixel/bevy_pixel_light.png")), | |
| Transform::from_xyz(-45., -20., 2.), | |
| Rotate, | |
| Pickable::default(), | |
| HIGH_RES_LAYERS, | |
| )).observe(trigger_debug_text::<Pointer<Over>>("Light".to_string())); | |
| } | |
| /// Spawns a capsule mesh on the pixel-perfect layer. | |
| fn setup_mesh( | |
| mut commands: Commands, | |
| mut meshes: ResMut<Assets<Mesh>>, | |
| mut materials: ResMut<Assets<ColorMaterial>>, | |
| ) { | |
| commands.spawn(( | |
| Mesh2d(meshes.add(Capsule2d::default())), | |
| MeshMaterial2d(materials.add(Color::BLACK)), | |
| Transform::from_xyz(25., 0., 2.).with_scale(Vec3::splat(32.)), | |
| Rotate, | |
| PIXEL_PERFECT_LAYERS, | |
| )).observe(trigger_debug_text::<Pointer<Over>>("Capsule".to_string())); | |
| } | |
| fn trigger_debug_text<E: EntityEvent>( | |
| text: String | |
| )-> impl Fn(On<E>){ | |
| move |_event|{ | |
| println!("{}", text); | |
| } | |
| } | |
| fn setup_camera(mut commands: Commands, mut images: ResMut<Assets<Image>>) { | |
| let canvas_size = Extent3d { | |
| width: RES_WIDTH, | |
| height: RES_HEIGHT, | |
| ..default() | |
| }; | |
| // This Image serves as a canvas representing the low-resolution game screen | |
| let mut canvas = Image { | |
| texture_descriptor: TextureDescriptor { | |
| label: None, | |
| size: canvas_size, | |
| dimension: TextureDimension::D2, | |
| format: TextureFormat::Bgra8UnormSrgb, | |
| mip_level_count: 1, | |
| sample_count: 1, | |
| usage: TextureUsages::TEXTURE_BINDING | |
| | TextureUsages::COPY_DST | |
| | TextureUsages::RENDER_ATTACHMENT, | |
| view_formats: &[], | |
| }, | |
| ..default() | |
| }; | |
| // Fill image.data with zeroes | |
| canvas.resize(canvas_size); | |
| let image_handle = images.add(canvas); | |
| // This camera renders whatever is on `PIXEL_PERFECT_LAYERS` to the canvas | |
| commands.spawn(( | |
| Camera2d, | |
| Camera { | |
| // Render before the "main pass" camera | |
| order: -1, | |
| clear_color: ClearColorConfig::Custom(GRAY.into()), | |
| ..default() | |
| }, | |
| RenderTarget::Image(image_handle.clone().into()), | |
| Msaa::Off, | |
| InGameCamera, | |
| Transform::from_xyz(0., 0., 0.), | |
| PIXEL_PERFECT_LAYERS, | |
| )); | |
| // Spawn the canvas | |
| commands.spawn((Sprite::from_image(image_handle), Canvas, HIGH_RES_LAYERS)); | |
| // The "outer" camera renders whatever is on `HIGH_RES_LAYERS` to the screen. | |
| // here, the canvas and one of the sample sprites will be rendered by this camera | |
| commands.spawn(( | |
| Camera2d, Msaa::Off, | |
| OuterCamera, | |
| HIGH_RES_LAYERS, | |
| Transform::from_xyz(0., 0., 0.0), | |
| )); | |
| } | |
| /// Rotates entities to demonstrate grid snapping. | |
| fn rotate(time: Res<Time>, mut transforms: Query<&mut Transform, With<Rotate>>) { | |
| for mut transform in &mut transforms { | |
| let dt = time.delta_secs(); | |
| transform.rotate_z(dt); | |
| } | |
| } | |
| /// Scales camera projection to fit the window (integer multiples only). | |
| fn fit_canvas( | |
| mut resize_messages: MessageReader<WindowResized>, | |
| mut projection: Single<&mut Projection, With<OuterCamera>>, | |
| ) { | |
| let Projection::Orthographic(projection) = &mut **projection else { | |
| return; | |
| }; | |
| for window_resized in resize_messages.read() { | |
| let h_scale = window_resized.width / RES_WIDTH as f32; | |
| let v_scale = window_resized.height / RES_HEIGHT as f32; | |
| projection.scale = 1. / h_scale.min(v_scale).round(); | |
| } | |
| } | |
| #[derive(Component)] | |
| struct VirtPointerMarker; | |
| pub struct VirtualPointerForLowResCamera; | |
| impl Plugin for VirtualPointerForLowResCamera { | |
| fn build(&self, app: &mut App){ | |
| app | |
| .add_systems(Startup, create_virtual_pointer) | |
| .add_systems(First, virtual_pointer_manipulation.after(PickingSystems::Input)) | |
| ; | |
| } | |
| } | |
| fn create_virtual_pointer( | |
| mut commands: Commands, | |
| ){ | |
| commands.spawn(( | |
| PointerId::Custom(Uuid::new_v4()), | |
| VirtPointerMarker | |
| )); | |
| } | |
| fn pointer_to_lowres_coords(position: Vec2, window_dimentions: Vec2) -> Option<Vec2>{ | |
| let x = position.x; | |
| let y = position.y; | |
| let x_max = window_dimentions.x; | |
| let y_max = window_dimentions.y; | |
| let x_ratio = x_max / RES_WIDTH as f32; | |
| let y_ratio = y_max / RES_HEIGHT as f32; | |
| let min_ratio = x_ratio.min(y_ratio); | |
| let fit_ratio = min_ratio.round(); | |
| let x_void_ratio = x_ratio - fit_ratio; | |
| let y_void_ratio = y_ratio - fit_ratio; | |
| let x_void = x_void_ratio * RES_WIDTH as f32; | |
| let y_void = y_void_ratio * RES_HEIGHT as f32; | |
| let x_min = x_void / 2.; | |
| let y_min = y_void / 2.; | |
| if x < x_min || x > x_max - x_min{ | |
| None | |
| } | |
| else if y < y_min || y > y_max - y_min{ | |
| None | |
| } else { | |
| let x_relative = x - x_min; | |
| let y_relative = y - y_min; | |
| Some( | |
| Vec2 { | |
| x: x_relative * (1. / fit_ratio), | |
| y: y_relative * (1. / fit_ratio) | |
| } | |
| ) | |
| } | |
| } | |
| fn virtual_pointer_manipulation( | |
| mut local_message_reader: Local<MessageCursor<PointerInput>>, | |
| mut messages: ResMut<Messages<PointerInput>>, | |
| virt_pointer: Single<&PointerId, With<VirtPointerMarker>>, | |
| custom_target: Single<&RenderTarget, With<InGameCamera>>, | |
| window: Single<&Window, With<PrimaryWindow>>, | |
| ){ | |
| let image_render_target = match *custom_target { | |
| RenderTarget::Image(image) => image, | |
| _ => return | |
| }; | |
| let mut to_send = vec![]; | |
| for msg in local_message_reader.read(&messages){ | |
| if msg.pointer_id == **virt_pointer{ | |
| continue; | |
| } | |
| let position = match pointer_to_lowres_coords( | |
| msg.location.position, | |
| Vec2 { x: window.width(), y: window.height() }, | |
| ) | |
| { | |
| Some(x) =>x, | |
| None => continue, | |
| }; | |
| let target = NormalizedRenderTarget::Image(image_render_target.clone()); | |
| to_send.push(PointerInput{ | |
| action: msg.action, | |
| location: Location{ | |
| target, | |
| position | |
| }, | |
| pointer_id: virt_pointer.clone(), | |
| }); | |
| } | |
| for msg in to_send { | |
| messages.write(msg); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment