docs/bevy/examples/app/externally_driven_headless_renderer.rs 4.7 K · 150 lines · raw · history

1 //! This example shows how to make an externally driven headless renderer,
2 //! pumping the update loop manually.
3 use bevy::{
4 app::SubApps,
5 asset::RenderAssetUsages,
6 camera::RenderTarget,
7 diagnostic::FrameCount,
8 image::Image,
9 prelude::*,
10 render::{
11 render_resource::{PollType, TextureFormat},
12 renderer::RenderDevice,
13 view::screenshot::{save_to_disk, Screenshot},
14 RenderPlugin,
15 },
16 window::ExitCondition,
17 winit::WinitPlugin,
18 };
19
20 fn main() {
21 let mut bw = BevyWrapper::new();
22
23 let target = bw.new_render_target(500, 500);
24 bw.spawn_camera(target.clone());
25 for i in 0..10 {
26 // Schedule a screenshot for this frame
27 bw.screenshot(target.clone(), i);
28 // Pump the update loop once
29 bw.update();
30 }
31 // Loop a couple times more to let screenshot gpu readback and then write to disk
32 bw.update();
33 bw.update();
34 }
35
36 struct BevyWrapper(SubApps);
37
38 impl BevyWrapper {
39 fn new() -> Self {
40 let render_plugin = RenderPlugin {
41 // Make sure all shaders are loaded for the first frame
42 synchronous_pipeline_compilation: true,
43 ..default()
44 };
45 // We don't have any windows, but the WindowPlugin is still needed
46 // because a lot of bevy expects it to be there. Just configure it
47 // to not have any windows and not exit automatically.
48 let window_plugin = WindowPlugin {
49 primary_window: None,
50 exit_condition: ExitCondition::DontExit,
51 ..default()
52 };
53
54 let mut app = App::new();
55 app.add_plugins(
56 DefaultPlugins
57 .set(window_plugin)
58 .set(render_plugin)
59 // Disable winit because we want to own the update loop ourselves.
60 .disable::<WinitPlugin>(),
61 )
62 .add_systems(Startup, spawn_test_scene)
63 .add_systems(Update, update_camera);
64
65 // We yeet the schedule runner and never call app.run(),
66 // so we have to finish and clean up ourselves
67 app.finish();
68 app.cleanup();
69
70 // We grab the sub apps cus we dont want the runner, as we'll
71 // be pumping the update loop ourselves manually.
72 Self(std::mem::take(app.sub_apps_mut()))
73 }
74
75 fn new_render_target(&mut self, width: u32, height: u32) -> RenderTarget {
76 let mut target =
77 Image::new_target_texture(width, height, TextureFormat::Rgba8UnormSrgb, None);
78 target.asset_usage = RenderAssetUsages::RENDER_WORLD;
79 self.0
80 .main
81 .world_mut()
82 .resource_mut::<Assets<Image>>()
83 .add(target)
84 .into()
85 }
86
87 fn spawn_camera(&mut self, target: RenderTarget) -> Entity {
88 self.0
89 .main
90 .world_mut()
91 .spawn((Camera3d::default(), target, Transform::IDENTITY))
92 .id()
93 }
94
95 // Run one world update and wait for rendering to finish.
96 fn update(&mut self) {
97 self.0.update();
98 // Wait for frame to finish rendering by wait polling the device
99 self.0
100 .main
101 .world()
102 .resource::<RenderDevice>()
103 .wgpu_device()
104 .poll(PollType::Wait {
105 submission_index: None,
106 timeout: None,
107 })
108 .unwrap();
109 }
110
111 // Schedules a screenshot to be captured on the next update.
112 fn screenshot(&mut self, target: RenderTarget, i: u32) {
113 self.0
114 .main
115 .world_mut()
116 .spawn(Screenshot::image(target.as_image().unwrap().clone()))
117 .observe(save_to_disk(format!("test_images/screenshot{i}.png")));
118 }
119 }
120
121 fn spawn_test_scene(
122 mut commands: Commands,
123 mut meshes: ResMut<Assets<Mesh>>,
124 mut materials: ResMut<Assets<StandardMaterial>>,
125 ) {
126 commands.spawn((
127 Mesh3d(meshes.add(Circle::new(4.0))),
128 MeshMaterial3d(materials.add(Color::WHITE)),
129 Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)),
130 ));
131 commands.spawn((
132 Mesh3d(meshes.add(Cuboid::new(2.0, 2.0, 2.0))),
133 MeshMaterial3d(materials.add(Color::srgb_u8(124, 144, 255))),
134 Transform::from_xyz(0.0, 1.0, 0.0),
135 ));
136 commands.spawn((
137 PointLight {
138 shadow_maps_enabled: true,
139 ..default()
140 },
141 Transform::from_xyz(4.0, 8.0, 4.0),
142 ));
143 }
144
145 fn update_camera(mut camera: Query<&mut Transform, With<Camera>>, frame_count: Res<FrameCount>) {
146 for mut t in camera.iter_mut() {
147 let (s, c) = ops::sin_cos(frame_count.0 as f32 * 0.3);
148 *t = Transform::from_xyz(s * 10.0, 4.5, c * 10.0).looking_at(Vec3::ZERO, Vec3::Y);
149 }
150 }