//! Renders the 3d scene to a texture, displays it in egui as a viewport, and adds picking support. use bevy::{prelude::*, render::render_resource::*}; use bevy_egui::*; use bevy_mod_picking::prelude::*; use picking_core::pointer::{InputMove, InputPress, Location}; fn main() { App::new() .add_plugins(( DefaultPlugins, DefaultPickingPlugins.build().disable::(), ViewportInputPlugin, EguiPlugin, )) .add_systems(Startup, setup_scene) .add_systems(Update, (ui, animate_scene)) .run(); } const VIEWPORT_SIZE: u32 = 256; /// Send this event to spawn a new viewport. #[derive(Event, Default)] pub struct SpawnViewport; /// Replaces bevy_mod_picking's default `InputPlugin` with inputs driven by egui, sending picking /// inputs when a pointer is over a viewport that has been rendered to a texture in the ui. struct ViewportInputPlugin; impl Plugin for ViewportInputPlugin { fn build(&self, app: &mut App) { app.add_event::() .add_systems(First, Self::send_mouse_clicks) // Default input plugin is disabled, we need to spawn a mouse pointer. .add_systems(Startup, input::mouse::spawn_mouse_pointer) .add_systems(Update, spawn_viewport.run_if(on_event::())); } } impl ViewportInputPlugin { fn send_mouse_clicks( mouse_inputs: Res>, mut pointer_press: EventWriter, ) { if mouse_inputs.just_pressed(MouseButton::Left) { pointer_press.send(InputPress { pointer_id: PointerId::Mouse, direction: pointer::PressDirection::Down, button: PointerButton::Primary, }); } else if mouse_inputs.just_released(MouseButton::Left) { pointer_press.send(InputPress { pointer_id: PointerId::Mouse, direction: pointer::PressDirection::Up, button: PointerButton::Primary, }); } } } #[derive(Component)] struct EguiViewport { bevy: Handle, egui: egui::TextureId, } fn ui( mut commands: Commands, mut egui_contexts: EguiContexts, egui_viewports: Query<(Entity, &EguiViewport)>, mut pointer_move: EventWriter, mut spawn_viewport: EventWriter, ) { egui::TopBottomPanel::top("menu_panel").show(egui_contexts.ctx_mut(), |ui| { egui::menu::bar(ui, |ui| { if ui.button("New Viewport").clicked() { spawn_viewport.send_default(); } }); }); // Draw each viewport in a window. This isn't as robust as it could be for the sake of // demonstration. This only works if the render target and egui texture are rendered at the same // resolution, and this completely ignores touch inputs and treats everything as a mouse input. for (viewport_entity, egui_viewport) in &egui_viewports { let mut is_open = true; egui::Window::new(format!("{:?}", viewport_entity)) .id(egui::Id::new(viewport_entity)) .open(&mut is_open) .show(egui_contexts.ctx_mut(), |ui| { // Draw the texture and get a response to check if a pointer is interacting let viewport_response = ui.add(egui::widgets::Image::new(egui::load::SizedTexture::new( egui_viewport.egui, [VIEWPORT_SIZE as f32, VIEWPORT_SIZE as f32], ))); if let Some(pointer_pos_window) = viewport_response.hover_pos() { // Compute the position of the pointer relative to the texture. let pos = pointer_pos_window - viewport_response.rect.min; pointer_move.send(InputMove { pointer_id: PointerId::Mouse, location: Location { target: bevy_render::camera::NormalizedRenderTarget::Image( egui_viewport.bevy.clone_weak(), ), position: Vec2::new(pos.x, pos.y), }, delta: Vec2::ZERO, }); } }); if !is_open { commands.entity(viewport_entity).despawn_recursive(); } } } /// Spawn a new camera to use as a viewport in egui on demand. fn spawn_viewport( mut egui_contexts: EguiContexts, mut commands: Commands, mut images: ResMut>, time: Res