Skip to content

Commit ca2073e

Browse files
authored
Add externally driven rendering example (#22551)
# Objective - Show how to drive loop for rendering externally. ## Solution - Add example. ## Testing Produces: <img width="410" height="187" alt="{EFA87DC0-0054-4EB4-8863-9DE13CC492D5}" src="https://github.com/user-attachments/assets/a9376e44-af22-4d98-80cc-02e14f0914d8" />
1 parent 3ffa26b commit ca2073e

File tree

3 files changed

+171
-0
lines changed

3 files changed

+171
-0
lines changed

Cargo.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1943,6 +1943,17 @@ description = "An application that runs with default plugins and displays an emp
19431943
category = "Application"
19441944
wasm = false
19451945

1946+
[[example]]
1947+
name = "externally_driven_headless_renderer"
1948+
path = "examples/app/externally_driven_headless_renderer.rs"
1949+
doc-scrape-examples = true
1950+
1951+
[package.metadata.example.externally_driven_headless_renderer]
1952+
name = "Externally Driven Headless Renderer"
1953+
description = "Using bevy with manually driven update to render images"
1954+
category = "Application"
1955+
wasm = false
1956+
19461957
[[example]]
19471958
name = "headless_renderer"
19481959
path = "examples/app/headless_renderer.rs"

examples/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,7 @@ Example | Description
233233
[Drag and Drop](../examples/app/drag_and_drop.rs) | An example that shows how to handle drag and drop in an app
234234
[Empty](../examples/app/empty.rs) | An empty application (does nothing)
235235
[Empty with Defaults](../examples/app/empty_defaults.rs) | An empty application with default plugins
236+
[Externally Driven Headless Renderer](../examples/app/externally_driven_headless_renderer.rs) | Using bevy with manually driven update to render images
236237
[Headless](../examples/app/headless.rs) | An application that runs without default plugins
237238
[Headless Renderer](../examples/app/headless_renderer.rs) | An application that runs with no window, but renders into image file
238239
[Log layers](../examples/app/log_layers.rs) | Illustrate how to add custom log layers
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
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::{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+
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 = 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+
130+
fn 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+
154+
fn 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+
}

0 commit comments

Comments
 (0)