Skip to content

Commit c6895db

Browse files
Initial world space UI support (#304)
* implement worldspace ui Signed-off-by: Schmarni <[email protected]> * clean render to texture logic from EguiNode Signed-off-by: Schmarni <[email protected]> * fix warnings Signed-off-by: Schmarni <[email protected]> * require render feature for rtt example Signed-off-by: Schmarni <[email protected]> * Implement paint callbacks for rendering to a texture * Fix compilation of not(feature = render) flag --------- Signed-off-by: Schmarni <[email protected]> Co-authored-by: Schmarni <[email protected]> Co-authored-by: Schmarni <[email protected]>
1 parent 0815a1d commit c6895db

8 files changed

Lines changed: 831 additions & 126 deletions

File tree

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ required-features = ["render"]
4040
[[example]]
4141
name = "ui"
4242
required-features = ["render"]
43+
[[example]]
44+
name = "render_egui_to_texture"
45+
required-features = ["render"]
4346

4447
[dependencies]
4548
bevy = { version = "0.14.0", default-features = false, features = [
@@ -48,6 +51,7 @@ bevy = { version = "0.14.0", default-features = false, features = [
4851
egui = { version = "0.28", default-features = false, features = ["bytemuck"] }
4952
bytemuck = "1"
5053
webbrowser = { version = "1.0.1", optional = true }
54+
wgpu-types = "0.20"
5155

5256
[target.'cfg(not(any(target_arch = "wasm32", target_os = "android")))'.dependencies]
5357
arboard = { version = "3.2.0", optional = true }

examples/paint_callback.rs

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,19 @@ use bevy::{
1313
};
1414
use bevy_egui::{
1515
egui_node::{EguiBevyPaintCallback, EguiBevyPaintCallbackImpl, EguiPipelineKey},
16-
EguiContexts, EguiPlugin,
16+
EguiContexts, EguiPlugin, EguiRenderToTextureHandle,
1717
};
1818
use std::path::Path;
19+
use wgpu_types::{Extent3d, TextureUsages};
1920

2021
fn main() {
2122
App::new()
2223
.add_plugins((DefaultPlugins, EguiPlugin, CustomPipelinePlugin))
23-
.add_systems(Update, ui_example_system)
24+
.add_systems(Startup, setup_worldspace)
25+
.add_systems(
26+
Update,
27+
(ui_example_system, ui_render_to_texture_example_system),
28+
)
2429
.run();
2530
}
2631

@@ -170,3 +175,64 @@ fn ui_example_system(mut ctx: EguiContexts) {
170175
});
171176
}
172177
}
178+
179+
// The following systems are used to render UI in world space to demonstrate that paint callbacks
180+
// work for them as well (they aren't needed to set up pain callbacks for regular screen-space UI,
181+
// so feel free to skip them):
182+
183+
fn setup_worldspace(
184+
mut images: ResMut<Assets<Image>>,
185+
mut meshes: ResMut<Assets<Mesh>>,
186+
mut materials: ResMut<Assets<StandardMaterial>>,
187+
mut commands: Commands,
188+
) {
189+
let output_texture = images.add({
190+
let size = Extent3d {
191+
width: 256,
192+
height: 256,
193+
depth_or_array_layers: 1,
194+
};
195+
let mut output_texture = Image {
196+
// You should use `0` so that the pixels are transparent.
197+
data: vec![0; (size.width * size.height * 4) as usize],
198+
..default()
199+
};
200+
output_texture.texture_descriptor.usage |= TextureUsages::RENDER_ATTACHMENT;
201+
output_texture.texture_descriptor.size = size;
202+
output_texture
203+
});
204+
205+
commands.spawn(PbrBundle {
206+
mesh: meshes.add(Cuboid::new(1.0, 1.0, 1.0).mesh()),
207+
material: materials.add(StandardMaterial {
208+
base_color: Color::WHITE,
209+
base_color_texture: Some(Handle::clone(&output_texture)),
210+
alpha_mode: AlphaMode::Blend,
211+
// Remove this if you want it to use the world's lighting.
212+
unlit: true,
213+
..default()
214+
}),
215+
..default()
216+
});
217+
commands.spawn(EguiRenderToTextureHandle(output_texture));
218+
commands.spawn(Camera3dBundle {
219+
transform: Transform::from_xyz(1.5, 1.5, 1.5).looking_at(Vec3::new(0., 0., 0.), Vec3::Y),
220+
..default()
221+
});
222+
}
223+
224+
fn ui_render_to_texture_example_system(
225+
mut contexts: Query<&mut bevy_egui::EguiContext, With<EguiRenderToTextureHandle>>,
226+
) {
227+
for mut ctx in contexts.iter_mut() {
228+
egui::Window::new("Worldspace UI").show(ctx.get_mut(), |ui| {
229+
let (resp, painter) =
230+
ui.allocate_painter(egui::Vec2 { x: 200., y: 200. }, egui::Sense::hover());
231+
232+
painter.add(EguiBevyPaintCallback::new_paint_callback(
233+
resp.rect,
234+
CustomPaintCallback,
235+
));
236+
});
237+
}
238+
}

examples/render_egui_to_texture.rs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
use bevy::prelude::*;
2+
use bevy_egui::{EguiContexts, EguiPlugin, EguiRenderToTextureHandle};
3+
use wgpu_types::{Extent3d, TextureUsages};
4+
5+
fn main() {
6+
let mut app = App::new();
7+
app.add_plugins(DefaultPlugins);
8+
app.add_plugins(EguiPlugin);
9+
app.add_systems(Startup, setup_worldspace);
10+
app.add_systems(Update, (update_screenspace, update_worldspace));
11+
app.run();
12+
}
13+
14+
fn update_screenspace(mut contexts: EguiContexts) {
15+
egui::Window::new("Screenspace UI").show(contexts.ctx_mut(), |ui| {
16+
ui.label("I'm rendering to screenspace!");
17+
});
18+
}
19+
20+
fn update_worldspace(
21+
mut contexts: Query<&mut bevy_egui::EguiContext, With<EguiRenderToTextureHandle>>,
22+
) {
23+
for mut ctx in contexts.iter_mut() {
24+
egui::Window::new("Worldspace UI").show(ctx.get_mut(), |ui| {
25+
ui.label("I'm rendering to a texture in worldspace!");
26+
});
27+
}
28+
}
29+
30+
fn setup_worldspace(
31+
mut images: ResMut<Assets<Image>>,
32+
mut meshes: ResMut<Assets<Mesh>>,
33+
mut materials: ResMut<Assets<StandardMaterial>>,
34+
mut commands: Commands,
35+
) {
36+
let output_texture = images.add({
37+
let size = Extent3d {
38+
width: 256,
39+
height: 256,
40+
depth_or_array_layers: 1,
41+
};
42+
let mut output_texture = Image {
43+
// You should use `0` so that the pixels are transparent.
44+
data: vec![0; (size.width * size.height * 4) as usize],
45+
..default()
46+
};
47+
output_texture.texture_descriptor.usage |= TextureUsages::RENDER_ATTACHMENT;
48+
output_texture.texture_descriptor.size = size;
49+
output_texture
50+
});
51+
52+
commands.spawn(PbrBundle {
53+
mesh: meshes.add(Cuboid::new(1.0, 1.0, 1.0).mesh()),
54+
material: materials.add(StandardMaterial {
55+
base_color: Color::WHITE,
56+
base_color_texture: Some(Handle::clone(&output_texture)),
57+
alpha_mode: AlphaMode::Blend,
58+
// Remove this if you want it to use the world's lighting.
59+
unlit: true,
60+
..default()
61+
}),
62+
..default()
63+
});
64+
commands.spawn(EguiRenderToTextureHandle(output_texture));
65+
commands.spawn(Camera3dBundle {
66+
transform: Transform::from_xyz(1.5, 1.5, 1.5).looking_at(Vec3::new(0., 0., 0.), Vec3::Y),
67+
..default()
68+
});
69+
}

src/egui_node.rs

Lines changed: 48 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use crate::{
22
render_systems::{
33
EguiPipelines, EguiTextureBindGroups, EguiTextureId, EguiTransform, EguiTransforms,
44
},
5-
EguiRenderOutput, EguiSettings, WindowSize,
5+
EguiRenderOutput, EguiSettings, RenderTargetSize,
66
};
77
use bevy::{
88
ecs::world::{FromWorld, World},
@@ -22,7 +22,10 @@ use bevy::{
2222
VertexBufferLayout, VertexFormat, VertexState, VertexStepMode,
2323
},
2424
renderer::{RenderContext, RenderDevice, RenderQueue},
25-
texture::{Image, ImageAddressMode, ImageFilterMode, ImageSampler, ImageSamplerDescriptor},
25+
texture::{
26+
GpuImage, Image, ImageAddressMode, ImageFilterMode, ImageSampler,
27+
ImageSamplerDescriptor,
28+
},
2629
view::{ExtractedWindow, ExtractedWindows},
2730
},
2831
};
@@ -96,12 +99,19 @@ pub struct EguiPipelineKey {
9699
}
97100

98101
impl EguiPipelineKey {
99-
/// Extracts target texture format in egui renderpass
102+
/// Constructs a pipeline key from a window.
100103
pub fn from_extracted_window(window: &ExtractedWindow) -> Option<Self> {
101104
Some(Self {
102105
texture_format: window.swap_chain_texture_format?.add_srgb_suffix(),
103106
})
104107
}
108+
109+
/// Constructs a pipeline key from a gpu image.
110+
pub fn from_gpu_image(image: &GpuImage) -> Self {
111+
EguiPipelineKey {
112+
texture_format: image.texture_format.add_srgb_suffix(),
113+
}
114+
}
105115
}
106116

107117
impl SpecializedRenderPipeline for EguiPipeline {
@@ -160,25 +170,24 @@ impl SpecializedRenderPipeline for EguiPipeline {
160170
}
161171
}
162172

163-
struct DrawCommand {
164-
clip_rect: egui::Rect,
165-
primitive: DrawPrimitive,
173+
pub(crate) struct DrawCommand {
174+
pub(crate) clip_rect: egui::Rect,
175+
pub(crate) primitive: DrawPrimitive,
166176
}
167177

168-
enum DrawPrimitive {
178+
pub(crate) enum DrawPrimitive {
169179
Egui(EguiDraw),
170180
PaintCallback(PaintCallbackDraw),
171181
}
172182

173-
struct PaintCallbackDraw {
174-
callback: std::sync::Arc<EguiBevyPaintCallback>,
175-
rect: egui::Rect,
183+
pub(crate) struct PaintCallbackDraw {
184+
pub(crate) callback: std::sync::Arc<EguiBevyPaintCallback>,
185+
pub(crate) rect: egui::Rect,
176186
}
177187

178-
#[derive(Debug)]
179-
struct EguiDraw {
180-
vertices_count: usize,
181-
egui_texture: EguiTextureId,
188+
pub(crate) struct EguiDraw {
189+
pub(crate) vertices_count: usize,
190+
pub(crate) egui_texture: EguiTextureId,
182191
}
183192

184193
/// Egui render node.
@@ -223,9 +232,10 @@ impl Node for EguiNode {
223232
return;
224233
};
225234

226-
let mut window_sizes = world.query::<(&WindowSize, &mut EguiRenderOutput)>();
235+
let mut render_target_size = world.query::<(&RenderTargetSize, &mut EguiRenderOutput)>();
227236

228-
let Ok((window_size, mut render_output)) = window_sizes.get_mut(world, self.window_entity)
237+
let Ok((window_size, mut render_output)) =
238+
render_target_size.get_mut(world, self.window_entity)
229239
else {
230240
return;
231241
};
@@ -382,21 +392,13 @@ impl Node for EguiNode {
382392
let pipeline_cache = world.get_resource::<PipelineCache>().unwrap();
383393

384394
let extracted_windows = &world.get_resource::<ExtractedWindows>().unwrap().windows;
385-
let extracted_window =
386-
if let Some(extracted_window) = extracted_windows.get(&self.window_entity) {
387-
extracted_window
388-
} else {
389-
return Ok(()); // No window
395+
let extracted_window = extracted_windows.get(&self.window_entity);
396+
let swap_chain_texture_view =
397+
match extracted_window.and_then(|v| v.swap_chain_texture_view.as_ref()) {
398+
None => return Ok(()),
399+
Some(window) => window,
390400
};
391401

392-
let swap_chain_texture_view = if let Some(swap_chain_texture_view) =
393-
extracted_window.swap_chain_texture_view.as_ref()
394-
{
395-
swap_chain_texture_view
396-
} else {
397-
return Ok(()); // No swapchain texture
398-
};
399-
400402
let render_queue = world.get_resource::<RenderQueue>().unwrap();
401403

402404
let (vertex_buffer, index_buffer) = match (&self.vertex_buffer, &self.index_buffer) {
@@ -432,13 +434,19 @@ impl Node for EguiNode {
432434
});
433435
let mut render_pass = TrackedRenderPass::new(device, render_pass);
434436

435-
let Some(key) = EguiPipelineKey::from_extracted_window(extracted_window) else {
436-
return Ok(());
437+
let (physical_width, physical_height, pipeline_key) = match extracted_window {
438+
Some(window) => (
439+
window.physical_width,
440+
window.physical_height,
441+
EguiPipelineKey::from_extracted_window(window),
442+
),
443+
None => unreachable!(),
437444
};
438-
439-
let Some(pipeline_id) = egui_pipelines.get(&extracted_window.entity) else {
445+
let Some(key) = pipeline_key else {
440446
return Ok(());
441447
};
448+
449+
let pipeline_id = egui_pipelines.get(&self.window_entity).unwrap();
442450
let Some(pipeline) = pipeline_cache.get_render_pipeline(*pipeline_id) else {
443451
return Ok(());
444452
};
@@ -454,8 +462,8 @@ impl Node for EguiNode {
454462
render_pass.set_viewport(
455463
0.,
456464
0.,
457-
extracted_window.physical_width as f32,
458-
extracted_window.physical_height as f32,
465+
physical_width as f32,
466+
physical_height as f32,
459467
0.,
460468
1.,
461469
);
@@ -479,11 +487,12 @@ impl Node for EguiNode {
479487
y: (draw_command.clip_rect.max.y * self.pixels_per_point).round() as u32,
480488
},
481489
};
490+
482491
let scrissor_rect = clip_urect.intersect(bevy::math::URect::new(
483492
0,
484493
0,
485-
extracted_window.physical_width,
486-
extracted_window.physical_width,
494+
physical_width,
495+
physical_height,
487496
));
488497
if scrissor_rect.is_empty() {
489498
continue;
@@ -529,10 +538,7 @@ impl Node for EguiNode {
529538
viewport: command.rect,
530539
clip_rect: draw_command.clip_rect,
531540
pixels_per_point: self.pixels_per_point,
532-
screen_size_px: [
533-
extracted_window.physical_width,
534-
extracted_window.physical_height,
535-
],
541+
screen_size_px: [physical_width, physical_height],
536542
};
537543

538544
let viewport = info.viewport_in_pixels();
@@ -649,7 +655,7 @@ impl EguiBevyPaintCallback {
649655
}
650656
}
651657

652-
fn cb(&self) -> &dyn EguiBevyPaintCallbackImpl {
658+
pub(crate) fn cb(&self) -> &dyn EguiBevyPaintCallbackImpl {
653659
self.0.as_ref()
654660
}
655661
}

0 commit comments

Comments
 (0)