Skip to main content

externally_driven_headless_renderer/
externally_driven_headless_renderer.rs

1//! This example shows how to make an externally driven headless renderer,
2//! pumping the update loop manually.
3use 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
20fn 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
36struct BevyWrapper(SubApps);
37
38impl 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
121fn 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
145fn 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}