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::{Extent3d, PollType, TextureDimension, TextureFormat, TextureUsages},
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 = Image::new_uninit(
77            Extent3d {
78                width,
79                height,
80                depth_or_array_layers: 1,
81            },
82            TextureDimension::D2,
83            TextureFormat::Rgba8UnormSrgb,
84            RenderAssetUsages::RENDER_WORLD,
85        );
86        // We're going to render to this image, mark it as such
87        target.texture_descriptor.usage |= TextureUsages::RENDER_ATTACHMENT;
88        self.0
89            .main
90            .world_mut()
91            .resource_mut::<Assets<Image>>()
92            .add(target)
93            .into()
94    }
95
96    fn spawn_camera(&mut self, target: RenderTarget) -> Entity {
97        self.0
98            .main
99            .world_mut()
100            .spawn((Camera3d::default(), target, Transform::IDENTITY))
101            .id()
102    }
103
104    // Run one world update and wait for rendering to finish.
105    fn update(&mut self) {
106        self.0.update();
107        // Wait for frame to finish rendering by wait polling the device
108        self.0
109            .main
110            .world()
111            .resource::<RenderDevice>()
112            .wgpu_device()
113            .poll(PollType::Wait {
114                submission_index: None,
115                timeout: None,
116            })
117            .unwrap();
118    }
119
120    // Schedules a screenshot to be captured on the next update.
121    fn screenshot(&mut self, target: RenderTarget, i: u32) {
122        self.0
123            .main
124            .world_mut()
125            .spawn(Screenshot::image(target.as_image().unwrap().clone()))
126            .observe(save_to_disk(format!("test_images/screenshot{i}.png")));
127    }
128}
129
130fn spawn_test_scene(
131    mut commands: Commands,
132    mut meshes: ResMut<Assets<Mesh>>,
133    mut materials: ResMut<Assets<StandardMaterial>>,
134) {
135    commands.spawn((
136        Mesh3d(meshes.add(Circle::new(4.0))),
137        MeshMaterial3d(materials.add(Color::WHITE)),
138        Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)),
139    ));
140    commands.spawn((
141        Mesh3d(meshes.add(Cuboid::new(2.0, 2.0, 2.0))),
142        MeshMaterial3d(materials.add(Color::srgb_u8(124, 144, 255))),
143        Transform::from_xyz(0.0, 1.0, 0.0),
144    ));
145    commands.spawn((
146        PointLight {
147            shadow_maps_enabled: true,
148            ..default()
149        },
150        Transform::from_xyz(4.0, 8.0, 4.0),
151    ));
152}
153
154fn update_camera(mut camera: Query<&mut Transform, With<Camera>>, frame_count: Res<FrameCount>) {
155    for mut t in camera.iter_mut() {
156        let (s, c) = ops::sin_cos(frame_count.0 as f32 * 0.3);
157        *t = Transform::from_xyz(s * 10.0, 4.5, c * 10.0).looking_at(Vec3::ZERO, Vec3::Y);
158    }
159}