bevy_core_pipeline/
fullscreen_material.rs

1//! This is mostly a pluginified version of the `custom_post_processing` example
2//!
3//! The plugin will create a new node that runs a fullscreen triangle.
4//!
5//! Users need to use the [`FullscreenMaterial`] trait to define the parameters like the graph label or the graph ordering.
6
7use core::any::type_name;
8use core::marker::PhantomData;
9
10use crate::{core_2d::graph::Core2d, core_3d::graph::Core3d, FullscreenShader};
11use bevy_app::{App, Plugin};
12use bevy_asset::AssetServer;
13use bevy_camera::{Camera2d, Camera3d};
14use bevy_ecs::{
15    component::Component,
16    entity::Entity,
17    query::{Added, Has, QueryItem},
18    resource::Resource,
19    system::{Commands, Res},
20    world::{FromWorld, World},
21};
22use bevy_image::BevyDefault;
23use bevy_render::{
24    extract_component::{
25        ComponentUniforms, DynamicUniformIndex, ExtractComponent, ExtractComponentPlugin,
26        UniformComponentPlugin,
27    },
28    render_graph::{
29        InternedRenderLabel, InternedRenderSubGraph, NodeRunError, RenderGraph, RenderGraphContext,
30        RenderGraphError, RenderGraphExt, RenderLabel, ViewNode, ViewNodeRunner,
31    },
32    render_resource::{
33        binding_types::{sampler, texture_2d, uniform_buffer},
34        encase::internal::WriteInto,
35        BindGroupEntries, BindGroupLayoutDescriptor, BindGroupLayoutEntries,
36        CachedRenderPipelineId, ColorTargetState, ColorWrites, FragmentState, Operations,
37        PipelineCache, RenderPassColorAttachment, RenderPassDescriptor, RenderPipelineDescriptor,
38        Sampler, SamplerBindingType, SamplerDescriptor, ShaderStages, ShaderType, TextureFormat,
39        TextureSampleType,
40    },
41    renderer::{RenderContext, RenderDevice},
42    view::ViewTarget,
43    ExtractSchedule, MainWorld, RenderApp, RenderStartup,
44};
45use bevy_shader::ShaderRef;
46use bevy_utils::default;
47use tracing::warn;
48
49#[derive(Default)]
50pub struct FullscreenMaterialPlugin<T: FullscreenMaterial> {
51    _marker: PhantomData<T>,
52}
53impl<T: FullscreenMaterial> Plugin for FullscreenMaterialPlugin<T> {
54    fn build(&self, app: &mut App) {
55        app.add_plugins((
56            ExtractComponentPlugin::<T>::default(),
57            UniformComponentPlugin::<T>::default(),
58        ));
59
60        let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
61            return;
62        };
63
64        render_app.add_systems(RenderStartup, init_pipeline::<T>);
65
66        if let Some(sub_graph) = T::sub_graph() {
67            render_app.add_render_graph_node::<ViewNodeRunner<FullscreenMaterialNode<T>>>(
68                sub_graph,
69                T::node_label(),
70            );
71
72            // We can't use add_render_graph_edges because it doesn't accept a Vec<RenderLabel>
73            if let Some(mut render_graph) = render_app.world_mut().get_resource_mut::<RenderGraph>()
74                && let Some(graph) = render_graph.get_sub_graph_mut(sub_graph)
75            {
76                for window in T::node_edges().windows(2) {
77                    let [a, b] = window else {
78                        break;
79                    };
80                    let Err(err) = graph.try_add_node_edge(*a, *b) else {
81                        continue;
82                    };
83                    match err {
84                        // Already existing edges are very easy to produce with this api
85                        // and shouldn't cause a panic
86                        RenderGraphError::EdgeAlreadyExists(_) => {}
87                        _ => panic!("{err:?}"),
88                    }
89                }
90            } else {
91                warn!("Failed to add edges for FullscreenMaterial");
92            };
93        } else {
94            // If there was no sub_graph specified we try to determine the graph based on the camera
95            // it gets added to.
96            render_app.add_systems(ExtractSchedule, extract_on_add::<T>);
97        }
98    }
99}
100
101fn extract_on_add<T: FullscreenMaterial>(world: &mut World) {
102    world.resource_scope::<MainWorld, ()>(|world, mut main_world| {
103        // Extract the material from the main world
104        let mut query =
105            main_world.query_filtered::<(Entity, Has<Camera3d>, Has<Camera2d>), Added<T>>();
106
107        // Create the node and add it to the render graph
108        world.resource_scope::<RenderGraph, ()>(|world, mut render_graph| {
109            for (_entity, is_3d, is_2d) in query.iter(&main_world) {
110                let graph = if is_3d && let Some(graph) = render_graph.get_sub_graph_mut(Core3d) {
111                    graph
112                } else if is_2d && let Some(graph) = render_graph.get_sub_graph_mut(Core2d) {
113                    graph
114                } else {
115                    warn!("FullscreenMaterial was added to an entity that isn't a camera");
116                    continue;
117                };
118
119                let node = ViewNodeRunner::<FullscreenMaterialNode<T>>::from_world(world);
120                graph.add_node(T::node_label(), node);
121
122                for window in T::node_edges().windows(2) {
123                    let [a, b] = window else {
124                        break;
125                    };
126                    let Err(err) = graph.try_add_node_edge(*a, *b) else {
127                        continue;
128                    };
129                    match err {
130                        // Already existing edges are very easy to produce with this api
131                        // and shouldn't cause a panic
132                        RenderGraphError::EdgeAlreadyExists(_) => {}
133                        _ => panic!("{err:?}"),
134                    }
135                }
136            }
137        });
138    });
139}
140
141/// A trait to define a material that will render to the entire screen using a fullscrene triangle
142pub trait FullscreenMaterial:
143    Component + ExtractComponent + Clone + Copy + ShaderType + WriteInto + Default
144{
145    /// The shader that will run on the entire screen using a fullscreen triangle
146    fn fragment_shader() -> ShaderRef;
147
148    /// The list of `node_edges`. In 3d, for a post processing effect, it would look like this:
149    ///
150    /// ```compile_fail
151    /// # use bevy_core_pipeline::core_3d::graph::Node3d;
152    /// # use bevy_render::render_graph::RenderLabel;
153    /// vec![
154    ///     Node3d::Tonemapping.intern(),
155    ///     // Self::sub_graph().intern(), // <--- your own label here
156    ///     Node3d::EndMainPassPostProcessing.intern(),
157    /// ]
158    /// ```
159    ///
160    /// This tell the render graph to run your fullscreen effect after the tonemapping pass but
161    /// before the end of post processing. For 2d, it would be the same but using Node2d. You can
162    /// specify any edges you want but make sure to include your own label.
163    fn node_edges() -> Vec<InternedRenderLabel>;
164
165    /// The [`bevy_render::render_graph::RenderSubGraph`] the effect will run in
166    ///
167    /// For 2d this is generally [`crate::core_2d::graph::Core2d`] and for 3d it's
168    /// [`crate::core_3d::graph::Core3d`]
169    fn sub_graph() -> Option<InternedRenderSubGraph> {
170        None
171    }
172
173    /// The label used to represent the render node that will run the pass
174    fn node_label() -> impl RenderLabel {
175        FullscreenMaterialLabel(type_name::<Self>())
176    }
177}
178
179#[derive(Debug, Hash, PartialEq, Eq, Clone)]
180struct FullscreenMaterialLabel(&'static str);
181
182impl RenderLabel for FullscreenMaterialLabel
183where
184    Self: 'static + Send + Sync + Clone + Eq + ::core::fmt::Debug + ::core::hash::Hash,
185{
186    fn dyn_clone(&self) -> Box<dyn RenderLabel> {
187        Box::new(::core::clone::Clone::clone(self))
188    }
189}
190
191#[derive(Resource)]
192struct FullscreenMaterialPipeline {
193    layout: BindGroupLayoutDescriptor,
194    sampler: Sampler,
195    pipeline_id: CachedRenderPipelineId,
196    pipeline_id_hdr: CachedRenderPipelineId,
197}
198
199fn init_pipeline<T: FullscreenMaterial>(
200    mut commands: Commands,
201    render_device: Res<RenderDevice>,
202    asset_server: Res<AssetServer>,
203    fullscreen_shader: Res<FullscreenShader>,
204    pipeline_cache: Res<PipelineCache>,
205) {
206    let layout = BindGroupLayoutDescriptor::new(
207        "post_process_bind_group_layout",
208        &BindGroupLayoutEntries::sequential(
209            ShaderStages::FRAGMENT,
210            (
211                // The screen texture
212                texture_2d(TextureSampleType::Float { filterable: true }),
213                // The sampler that will be used to sample the screen texture
214                sampler(SamplerBindingType::Filtering),
215                // We use a uniform buffer so users can pass some data to the effect
216                // Eventually we should just use a separate bind group for user data
217                uniform_buffer::<T>(true),
218            ),
219        ),
220    );
221    let sampler = render_device.create_sampler(&SamplerDescriptor::default());
222    let shader = match T::fragment_shader() {
223        ShaderRef::Default => {
224            // TODO not sure what an actual fallback should be. An empty shader or output a solid
225            // color to indicate a missing shader?
226            unimplemented!(
227                "FullscreenMaterial::fragment_shader() must not return ShaderRef::Default"
228            )
229        }
230        ShaderRef::Handle(handle) => handle,
231        ShaderRef::Path(path) => asset_server.load(path),
232    };
233    // Setup a fullscreen triangle for the vertex state.
234    let vertex_state = fullscreen_shader.to_vertex_state();
235    let mut desc = RenderPipelineDescriptor {
236        label: Some("post_process_pipeline".into()),
237        layout: vec![layout.clone()],
238        vertex: vertex_state,
239        fragment: Some(FragmentState {
240            shader,
241            targets: vec![Some(ColorTargetState {
242                format: TextureFormat::bevy_default(),
243                blend: None,
244                write_mask: ColorWrites::ALL,
245            })],
246            ..default()
247        }),
248        ..default()
249    };
250    let pipeline_id = pipeline_cache.queue_render_pipeline(desc.clone());
251    desc.fragment.as_mut().unwrap().targets[0]
252        .as_mut()
253        .unwrap()
254        .format = ViewTarget::TEXTURE_FORMAT_HDR;
255    let pipeline_id_hdr = pipeline_cache.queue_render_pipeline(desc);
256    commands.insert_resource(FullscreenMaterialPipeline {
257        layout,
258        sampler,
259        pipeline_id,
260        pipeline_id_hdr,
261    });
262}
263
264#[derive(Default)]
265struct FullscreenMaterialNode<T: FullscreenMaterial> {
266    _marker: PhantomData<T>,
267}
268
269impl<T: FullscreenMaterial> ViewNode for FullscreenMaterialNode<T> {
270    // TODO we should expose the depth buffer and the gbuffer if using deferred
271    type ViewQuery = (&'static ViewTarget, &'static DynamicUniformIndex<T>);
272
273    fn run<'w>(
274        &self,
275        _graph: &mut RenderGraphContext,
276        render_context: &mut RenderContext,
277        (view_target, settings_index): QueryItem<Self::ViewQuery>,
278        world: &World,
279    ) -> Result<(), NodeRunError> {
280        let fullscreen_pipeline = world.resource::<FullscreenMaterialPipeline>();
281
282        let pipeline_cache = world.resource::<PipelineCache>();
283        let pipeline_id = if view_target.is_hdr() {
284            fullscreen_pipeline.pipeline_id_hdr
285        } else {
286            fullscreen_pipeline.pipeline_id
287        };
288
289        let Some(pipeline) = pipeline_cache.get_render_pipeline(pipeline_id) else {
290            return Ok(());
291        };
292
293        let data_uniforms = world.resource::<ComponentUniforms<T>>();
294        let Some(settings_binding) = data_uniforms.uniforms().binding() else {
295            return Ok(());
296        };
297
298        let post_process = view_target.post_process_write();
299
300        let bind_group = render_context.render_device().create_bind_group(
301            "post_process_bind_group",
302            &pipeline_cache.get_bind_group_layout(&fullscreen_pipeline.layout),
303            &BindGroupEntries::sequential((
304                post_process.source,
305                &fullscreen_pipeline.sampler,
306                settings_binding.clone(),
307            )),
308        );
309
310        let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor {
311            label: Some("post_process_pass"),
312            color_attachments: &[Some(RenderPassColorAttachment {
313                view: post_process.destination,
314                depth_slice: None,
315                resolve_target: None,
316                ops: Operations::default(),
317            })],
318            depth_stencil_attachment: None,
319            timestamp_writes: None,
320            occlusion_query_set: None,
321        });
322
323        render_pass.set_render_pipeline(pipeline);
324        render_pass.set_bind_group(0, &bind_group, &[settings_index.index()]);
325        render_pass.draw(0..3, 0..1);
326
327        Ok(())
328    }
329}