bevy_egui/render/
mod.rs

1pub use render_pass::*;
2
3/// Defines Egui node graph.
4pub mod graph {
5    use bevy_render::render_graph::{RenderLabel, RenderSubGraph};
6
7    /// Egui subgraph (is run by [`super::RunEguiSubgraphOnEguiViewNode`]).
8    #[derive(Debug, Hash, PartialEq, Eq, Clone, RenderSubGraph)]
9    pub struct SubGraphEgui;
10
11    /// Egui node defining the Egui rendering pass.
12    #[derive(Debug, Hash, PartialEq, Eq, Clone, RenderLabel)]
13    pub enum NodeEgui {
14        /// Egui rendering pass.
15        EguiPass,
16    }
17}
18
19use crate::{
20    EguiContextSettings, EguiRenderOutput, RenderComputedScaleFactor,
21    render::graph::{NodeEgui, SubGraphEgui},
22};
23use bevy_app::SubApp;
24use bevy_asset::{Handle, RenderAssetUsages, uuid_handle};
25use bevy_camera::Camera;
26use bevy_ecs::{
27    component::Component,
28    entity::Entity,
29    query::Has,
30    resource::Resource,
31    system::{Commands, Local, ResMut},
32    world::{FromWorld, World},
33};
34use bevy_image::{
35    BevyDefault, Image, ImageAddressMode, ImageFilterMode, ImageSampler, ImageSamplerDescriptor,
36};
37use bevy_math::{Mat4, UVec4};
38use bevy_mesh::VertexBufferLayout;
39use bevy_platform::collections::HashSet;
40use bevy_render::{
41    MainWorld,
42    render_graph::{Node, NodeRunError, RenderGraph, RenderGraphContext},
43    render_phase::TrackedRenderPass,
44    render_resource::{
45        BindGroupLayout, BindGroupLayoutEntries, FragmentState, RenderPipelineDescriptor,
46        SpecializedRenderPipeline, VertexState,
47        binding_types::{sampler, texture_2d, uniform_buffer},
48    },
49    renderer::{RenderContext, RenderDevice},
50    sync_world::{RenderEntity, TemporaryRenderEntity},
51    view::{ExtractedView, Hdr, RetainedViewEntity, ViewTarget},
52};
53use bevy_shader::{Shader, ShaderDefVal};
54use egui::{TextureFilter, TextureOptions};
55use std::num::NonZero;
56use systems::{EguiTextureId, EguiTransform};
57use wgpu_types::{
58    BlendState, ColorTargetState, ColorWrites, Extent3d, MultisampleState, PrimitiveState,
59    PushConstantRange, SamplerBindingType, ShaderStages, TextureDimension, TextureFormat,
60    TextureSampleType, VertexFormat, VertexStepMode,
61};
62
63mod render_pass;
64/// Plugin systems for the render app.
65#[cfg(feature = "render")]
66pub mod systems;
67
68/// A render-world component that lives on the main render target view and
69/// specifies the corresponding Egui view.
70///
71/// For example, if Egui is being rendered to a 3D camera, this component lives on
72/// the 3D camera and contains the entity corresponding to the Egui view.
73///
74/// Entity id of the temporary render entity with the corresponding extracted Egui view.
75#[derive(Component, Debug)]
76pub struct EguiCameraView(pub Entity);
77
78/// A render-world component that lives on the Egui view and specifies the
79/// corresponding main render target view.
80///
81/// For example, if Egui is being rendered to a 3D camera, this component
82/// lives on the Egui view and contains the entity corresponding to the 3D camera.
83///
84/// This is the inverse of [`EguiCameraView`].
85#[derive(Component, Debug)]
86pub struct EguiViewTarget(pub Entity);
87
88/// Adds and returns an Egui subgraph.
89pub fn get_egui_graph(render_app: &mut SubApp) -> RenderGraph {
90    let pass_node = EguiPassNode::new(render_app.world_mut());
91    let mut graph = RenderGraph::default();
92    graph.add_node(NodeEgui::EguiPass, pass_node);
93    graph
94}
95
96/// A [`Node`] that executes the Egui rendering subgraph on the Egui view.
97pub struct RunEguiSubgraphOnEguiViewNode;
98
99impl Node for RunEguiSubgraphOnEguiViewNode {
100    fn run<'w>(
101        &self,
102        graph: &mut RenderGraphContext,
103        _: &mut RenderContext<'w>,
104        world: &'w World,
105    ) -> Result<(), NodeRunError> {
106        // Fetch the UI view.
107        let Some(mut render_views) = world.try_query::<&EguiCameraView>() else {
108            return Ok(());
109        };
110        let Ok(default_camera_view) = render_views.get(world, graph.view_entity()) else {
111            return Ok(());
112        };
113
114        // Run the subgraph on the Egui view.
115        graph.run_sub_graph(SubGraphEgui, vec![], Some(default_camera_view.0))?;
116        Ok(())
117    }
118}
119
120/// Extracts all Egui contexts associated with a camera into the render world.
121pub fn extract_egui_camera_view_system(
122    mut commands: Commands,
123    mut world: ResMut<MainWorld>,
124    mut live_entities: Local<HashSet<RetainedViewEntity>>,
125) {
126    live_entities.clear();
127    let mut q = world.query::<(
128        Entity,
129        RenderEntity,
130        &Camera,
131        &mut EguiRenderOutput,
132        &EguiContextSettings,
133        Has<Hdr>,
134    )>();
135
136    for (main_entity, render_entity, camera, mut egui_render_output, settings, hdr) in
137        &mut q.iter_mut(&mut world)
138    {
139        // Move Egui shapes and textures out of the main world into the render one.
140        let egui_render_output = std::mem::take(egui_render_output.as_mut());
141
142        // Ignore inactive cameras.
143        if !camera.is_active {
144            commands
145                .get_entity(render_entity)
146                .expect("Camera entity wasn't synced.")
147                .remove::<EguiCameraView>();
148            continue;
149        }
150
151        const UI_CAMERA_FAR: f32 = 1000.0;
152        const EGUI_CAMERA_SUBVIEW: u32 = 2095931312;
153        const UI_CAMERA_TRANSFORM_OFFSET: f32 = -0.1;
154
155        if let Some(physical_viewport_rect) = camera.physical_viewport_rect() {
156            // Use a projection matrix with the origin in the top left instead of the bottom left that comes with OrthographicProjection.
157            let projection_matrix = Mat4::orthographic_rh(
158                0.0,
159                physical_viewport_rect.width() as f32,
160                physical_viewport_rect.height() as f32,
161                0.0,
162                0.0,
163                UI_CAMERA_FAR,
164            );
165            // We use `EGUI_CAMERA_SUBVIEW` here so as not to conflict with the
166            // main 3D or 2D camera or UI view, which will have subview index 0 or 1.
167            let retained_view_entity =
168                RetainedViewEntity::new(main_entity.into(), None, EGUI_CAMERA_SUBVIEW);
169            // Creates the UI view.
170            let ui_camera_view = commands
171                .spawn((
172                    ExtractedView {
173                        retained_view_entity,
174                        clip_from_view: projection_matrix,
175                        world_from_view: bevy_transform::components::GlobalTransform::from_xyz(
176                            0.0,
177                            0.0,
178                            UI_CAMERA_FAR + UI_CAMERA_TRANSFORM_OFFSET,
179                        ),
180                        clip_from_world: None,
181                        hdr,
182                        viewport: UVec4::from((
183                            physical_viewport_rect.min,
184                            physical_viewport_rect.size(),
185                        )),
186                        color_grading: Default::default(),
187                    },
188                    // Link to the main camera view.
189                    EguiViewTarget(render_entity),
190                    egui_render_output,
191                    RenderComputedScaleFactor {
192                        scale_factor: settings.scale_factor
193                            * camera.target_scaling_factor().unwrap_or(1.0),
194                    },
195                    TemporaryRenderEntity,
196                ))
197                .id();
198
199            let mut entity_commands = commands
200                .get_entity(render_entity)
201                .expect("Camera entity wasn't synced.");
202            // Link from the main 2D/3D camera view to the UI view.
203            entity_commands.insert(EguiCameraView(ui_camera_view));
204            live_entities.insert(retained_view_entity);
205        }
206    }
207}
208
209/// Egui shader.
210pub const EGUI_SHADER_HANDLE: Handle<Shader> = uuid_handle!("05a4d7a0-4f24-4d7f-b606-3f399074261f");
211
212/// Egui render settings.
213#[derive(Resource)]
214pub struct EguiRenderSettings {
215    /// See [`super::EguiPlugin`] for setting description.
216    pub bindless_mode_array_size: Option<NonZero<u32>>,
217}
218
219/// Egui render pipeline.
220#[derive(Resource)]
221pub struct EguiPipeline {
222    /// Transform bind group layout.
223    pub transform_bind_group_layout: BindGroupLayout,
224    /// Texture bind group layout.
225    pub texture_bind_group_layout: BindGroupLayout,
226    /// Is bindless rendering mode enabled
227    /// and how many textures can be rendered in one bind group.
228    pub bindless: Option<NonZero<u32>>,
229}
230
231impl FromWorld for EguiPipeline {
232    fn from_world(render_world: &mut World) -> Self {
233        let render_device = render_world.resource::<RenderDevice>();
234        let settings = render_world.resource::<EguiRenderSettings>();
235
236        let features = render_device.features();
237
238        // TODO: In wgpu 0.26
239        // Check: max_binding_array_elements_per_shader_stage and
240        // max_binding_array_sampler_elements-per_shader_stage
241        // to be sure that device support provided limits
242        let bindless = if features.contains(wgpu_types::Features::TEXTURE_BINDING_ARRAY)
243            && features.contains(wgpu_types::Features::PUSH_CONSTANTS)
244        {
245            settings.bindless_mode_array_size
246        } else {
247            None
248        };
249
250        let transform_bind_group_layout = render_device.create_bind_group_layout(
251            "egui_transform_layout",
252            &BindGroupLayoutEntries::single(
253                ShaderStages::VERTEX,
254                uniform_buffer::<EguiTransform>(true),
255            ),
256        );
257
258        let texture_bind_group_layout = if let Some(bindless) = bindless {
259            render_device.create_bind_group_layout(
260                "egui_texture_layout",
261                &BindGroupLayoutEntries::sequential(
262                    ShaderStages::FRAGMENT,
263                    (
264                        texture_2d(TextureSampleType::Float { filterable: true }).count(bindless),
265                        sampler(SamplerBindingType::Filtering).count(bindless),
266                    ),
267                ),
268            )
269        } else {
270            render_device.create_bind_group_layout(
271                "egui_texture_layout",
272                &BindGroupLayoutEntries::sequential(
273                    ShaderStages::FRAGMENT,
274                    (
275                        texture_2d(TextureSampleType::Float { filterable: true }),
276                        sampler(SamplerBindingType::Filtering),
277                    ),
278                ),
279            )
280        };
281
282        EguiPipeline {
283            transform_bind_group_layout,
284            texture_bind_group_layout,
285            bindless,
286        }
287    }
288}
289
290/// Key for specialized pipeline.
291#[derive(PartialEq, Eq, Hash, Clone, Copy)]
292pub struct EguiPipelineKey {
293    /// Equals `true` for cameras that have the [`Hdr`] component.
294    pub hdr: bool,
295}
296
297impl SpecializedRenderPipeline for EguiPipeline {
298    type Key = EguiPipelineKey;
299
300    fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
301        let mut shader_defs = Vec::new();
302        let mut push_constant_ranges = Vec::new();
303
304        if let Some(bindless) = self.bindless {
305            shader_defs.push(ShaderDefVal::UInt("BINDLESS".into(), u32::from(bindless)));
306            push_constant_ranges.push(PushConstantRange {
307                stages: ShaderStages::FRAGMENT,
308                range: 0..4,
309            });
310        }
311
312        RenderPipelineDescriptor {
313            label: Some("egui_pipeline".into()),
314            layout: vec![
315                self.transform_bind_group_layout.clone(),
316                self.texture_bind_group_layout.clone(),
317            ],
318            vertex: VertexState {
319                shader: EGUI_SHADER_HANDLE,
320                shader_defs: shader_defs.clone(),
321                entry_point: Some("vs_main".into()),
322                buffers: vec![VertexBufferLayout::from_vertex_formats(
323                    VertexStepMode::Vertex,
324                    [
325                        VertexFormat::Float32x2, // position
326                        VertexFormat::Float32x2, // UV
327                        VertexFormat::Unorm8x4,  // color (sRGB)
328                    ],
329                )],
330            },
331            fragment: Some(FragmentState {
332                shader: EGUI_SHADER_HANDLE,
333                shader_defs,
334                entry_point: Some("fs_main".into()),
335                targets: vec![Some(ColorTargetState {
336                    format: if key.hdr {
337                        ViewTarget::TEXTURE_FORMAT_HDR
338                    } else {
339                        TextureFormat::bevy_default()
340                    },
341                    blend: Some(BlendState::PREMULTIPLIED_ALPHA_BLENDING),
342                    write_mask: ColorWrites::ALL,
343                })],
344            }),
345            primitive: PrimitiveState::default(),
346            depth_stencil: None,
347            multisample: MultisampleState::default(),
348            push_constant_ranges,
349            zero_initialize_workgroup_memory: false,
350        }
351    }
352}
353
354pub(crate) struct DrawCommand {
355    pub(crate) clip_rect: egui::Rect,
356    pub(crate) primitive: DrawPrimitive,
357}
358
359pub(crate) enum DrawPrimitive {
360    Egui(EguiDraw),
361    PaintCallback(PaintCallbackDraw),
362}
363
364pub(crate) struct PaintCallbackDraw {
365    pub(crate) callback: std::sync::Arc<EguiBevyPaintCallback>,
366    pub(crate) rect: egui::Rect,
367}
368
369pub(crate) struct EguiDraw {
370    pub(crate) vertices_count: usize,
371    pub(crate) egui_texture: EguiTextureId,
372}
373
374pub(crate) fn as_color_image(image: &egui::ImageData) -> egui::ColorImage {
375    match image {
376        egui::ImageData::Color(image) => (**image).clone(),
377    }
378}
379
380pub(crate) fn color_image_as_bevy_image(
381    egui_image: &egui::ColorImage,
382    sampler_descriptor: ImageSampler,
383) -> Image {
384    let pixels = egui_image
385        .pixels
386        .iter()
387        // We unmultiply Egui textures to premultiply them later in the fragment shader.
388        // As user textures loaded as Bevy assets are not premultiplied (and there seems to be no
389        // convenient way to convert them to premultiplied ones), we do this with Egui ones.
390        .flat_map(|color| color.to_srgba_unmultiplied())
391        .collect();
392
393    Image {
394        sampler: sampler_descriptor,
395        ..Image::new(
396            Extent3d {
397                width: egui_image.width() as u32,
398                height: egui_image.height() as u32,
399                depth_or_array_layers: 1,
400            },
401            TextureDimension::D2,
402            pixels,
403            TextureFormat::Rgba8UnormSrgb,
404            RenderAssetUsages::MAIN_WORLD | RenderAssetUsages::RENDER_WORLD,
405        )
406    }
407}
408
409pub(crate) fn texture_options_as_sampler_descriptor(
410    options: &TextureOptions,
411) -> ImageSamplerDescriptor {
412    fn convert_filter(filter: &TextureFilter) -> ImageFilterMode {
413        match filter {
414            egui::TextureFilter::Nearest => ImageFilterMode::Nearest,
415            egui::TextureFilter::Linear => ImageFilterMode::Linear,
416        }
417    }
418    let address_mode = match options.wrap_mode {
419        egui::TextureWrapMode::ClampToEdge => ImageAddressMode::ClampToEdge,
420        egui::TextureWrapMode::Repeat => ImageAddressMode::Repeat,
421        egui::TextureWrapMode::MirroredRepeat => ImageAddressMode::MirrorRepeat,
422    };
423    ImageSamplerDescriptor {
424        mag_filter: convert_filter(&options.magnification),
425        min_filter: convert_filter(&options.minification),
426        address_mode_u: address_mode,
427        address_mode_v: address_mode,
428        ..Default::default()
429    }
430}
431
432/// Callback to execute custom 'wgpu' rendering inside [`EguiPassNode`] render graph node.
433///
434/// Rendering can be implemented using for example:
435/// * native wgpu rendering libraries,
436/// * or with [`bevy_render::render_phase`] approach.
437pub struct EguiBevyPaintCallback(Box<dyn EguiBevyPaintCallbackImpl>);
438
439impl EguiBevyPaintCallback {
440    /// Creates a new [`egui::epaint::PaintCallback`] from a callback trait instance.
441    pub fn new_paint_callback<T>(rect: egui::Rect, callback: T) -> egui::epaint::PaintCallback
442    where
443        T: EguiBevyPaintCallbackImpl + 'static,
444    {
445        let callback = Self(Box::new(callback));
446        egui::epaint::PaintCallback {
447            rect,
448            callback: std::sync::Arc::new(callback),
449        }
450    }
451
452    pub(crate) fn cb(&self) -> &dyn EguiBevyPaintCallbackImpl {
453        self.0.as_ref()
454    }
455}
456
457/// Callback that executes custom rendering logic
458pub trait EguiBevyPaintCallbackImpl: Send + Sync {
459    /// Paint callback will be rendered in near future, all data must be finalized for render step
460    fn update(
461        &self,
462        info: egui::PaintCallbackInfo,
463        render_entity: RenderEntity,
464        pipeline_key: EguiPipelineKey,
465        world: &mut World,
466    );
467
468    /// Paint callback call before render step
469    ///
470    ///
471    /// Can be used to implement custom render passes
472    /// or to submit command buffers for execution before egui render pass
473    fn prepare_render<'w>(
474        &self,
475        info: egui::PaintCallbackInfo,
476        render_context: &mut RenderContext<'w>,
477        render_entity: RenderEntity,
478        pipeline_key: EguiPipelineKey,
479        world: &'w World,
480    ) {
481        let _ = (info, render_context, render_entity, pipeline_key, world);
482        // Do nothing by default
483    }
484
485    /// Paint callback render step
486    ///
487    /// Native wgpu RenderPass can be retrieved from [`TrackedRenderPass`] by calling
488    /// [`TrackedRenderPass::wgpu_pass`].
489    fn render<'pass>(
490        &self,
491        info: egui::PaintCallbackInfo,
492        render_pass: &mut TrackedRenderPass<'pass>,
493        render_entity: RenderEntity,
494        pipeline_key: EguiPipelineKey,
495        world: &'pass World,
496    );
497}