//! This example shows how to make an externally driven headless renderer, //! pumping the update loop manually. use bevy::{ app::SubApps, asset::RenderAssetUsages, camera::RenderTarget, diagnostic::FrameCount, image::Image, prelude::*, render::{ render_resource::{PollType, TextureFormat}, renderer::RenderDevice, view::screenshot::{save_to_disk, Screenshot}, RenderPlugin, }, window::ExitCondition, winit::WinitPlugin, }; fn main() { let mut bw = BevyWrapper::new(); let target = bw.new_render_target(500, 500); bw.spawn_camera(target.clone()); for i in 0..10 { // Schedule a screenshot for this frame bw.screenshot(target.clone(), i); // Pump the update loop once bw.update(); } // Loop a couple times more to let screenshot gpu readback and then write to disk bw.update(); bw.update(); } struct BevyWrapper(SubApps); impl BevyWrapper { fn new() -> Self { let render_plugin = RenderPlugin { // Make sure all shaders are loaded for the first frame synchronous_pipeline_compilation: true, ..default() }; // We don't have any windows, but the WindowPlugin is still needed // because a lot of bevy expects it to be there. Just configure it // to not have any windows and not exit automatically. let window_plugin = WindowPlugin { primary_window: None, exit_condition: ExitCondition::DontExit, ..default() }; let mut app = App::new(); app.add_plugins( DefaultPlugins .set(window_plugin) .set(render_plugin) // Disable winit because we want to own the update loop ourselves. .disable::(), ) .add_systems(Startup, spawn_test_scene) .add_systems(Update, update_camera); // We yeet the schedule runner and never call app.run(), // so we have to finish and clean up ourselves app.finish(); app.cleanup(); // We grab the sub apps cus we dont want the runner, as we'll // be pumping the update loop ourselves manually. Self(std::mem::take(app.sub_apps_mut())) } fn new_render_target(&mut self, width: u32, height: u32) -> RenderTarget { let mut target = Image::new_target_texture(width, height, TextureFormat::Rgba8UnormSrgb, None); target.asset_usage = RenderAssetUsages::RENDER_WORLD; self.0 .main .world_mut() .resource_mut::>() .add(target) .into() } fn spawn_camera(&mut self, target: RenderTarget) -> Entity { self.0 .main .world_mut() .spawn((Camera3d::default(), target, Transform::IDENTITY)) .id() } // Run one world update and wait for rendering to finish. fn update(&mut self) { self.0.update(); // Wait for frame to finish rendering by wait polling the device self.0 .main .world() .resource::() .wgpu_device() .poll(PollType::Wait { submission_index: None, timeout: None, }) .unwrap(); } // Schedules a screenshot to be captured on the next update. fn screenshot(&mut self, target: RenderTarget, i: u32) { self.0 .main .world_mut() .spawn(Screenshot::image(target.as_image().unwrap().clone())) .observe(save_to_disk(format!("test_images/screenshot{i}.png"))); } } fn spawn_test_scene( mut commands: Commands, mut meshes: ResMut>, mut materials: ResMut>, ) { commands.spawn(( Mesh3d(meshes.add(Circle::new(4.0))), MeshMaterial3d(materials.add(Color::WHITE)), Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)), )); commands.spawn(( Mesh3d(meshes.add(Cuboid::new(2.0, 2.0, 2.0))), MeshMaterial3d(materials.add(Color::srgb_u8(124, 144, 255))), Transform::from_xyz(0.0, 1.0, 0.0), )); commands.spawn(( PointLight { shadow_maps_enabled: true, ..default() }, Transform::from_xyz(4.0, 8.0, 4.0), )); } fn update_camera(mut camera: Query<&mut Transform, With>, frame_count: Res) { for mut t in camera.iter_mut() { let (s, c) = ops::sin_cos(frame_count.0 as f32 * 0.3); *t = Transform::from_xyz(s * 10.0, 4.5, c * 10.0).looking_at(Vec3::ZERO, Vec3::Y); } }