Skip to main content

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 system that runs a fullscreen triangle.
4//!
5//! Users need to use the [`FullscreenMaterial`] trait to define the parameters like ordering.
6
7use core::any::type_name;
8use core::marker::PhantomData;
9
10use crate::{schedule::Core3d, tonemapping::tonemapping, Core3dSystems, FullscreenShader};
11use bevy_app::{App, Plugin};
12use bevy_asset::AssetServer;
13use bevy_ecs::{
14    component::Component,
15    entity::Entity,
16    error::BevyError,
17    query::With,
18    resource::Resource,
19    schedule::{IntoScheduleConfigs, ScheduleConfigs, ScheduleLabel},
20    system::{BoxedSystem, Commands, Query, Res, ResMut},
21};
22use bevy_render::{
23    camera::ExtractedCamera,
24    extract_component::{
25        ComponentUniforms, DynamicUniformIndex, ExtractComponent, ExtractComponentPlugin,
26        UniformComponentPlugin,
27    },
28    render_resource::{
29        binding_types::{sampler, texture_2d, uniform_buffer},
30        encase::internal::WriteInto,
31        BindGroup, BindGroupEntries, BindGroupLayoutDescriptor, BindGroupLayoutEntries,
32        CachedRenderPipelineId, Canonical, ColorTargetState, ColorWrites, FragmentState,
33        Operations, PipelineCache, RenderPassColorAttachment, RenderPassDescriptor, RenderPipeline,
34        RenderPipelineDescriptor, Sampler, SamplerBindingType, SamplerDescriptor, ShaderStages,
35        ShaderType, Specializer, SpecializerKey, TextureFormat, TextureSampleType, TextureView,
36        TextureViewId, Variants,
37    },
38    renderer::{RenderContext, RenderDevice, ViewQuery},
39    view::{ExtractedView, ViewTarget},
40    Render, RenderApp, RenderStartup, RenderSystems,
41};
42use bevy_shader::ShaderRef;
43use bevy_utils::default;
44
45#[derive(Default)]
46pub struct FullscreenMaterialPlugin<T: FullscreenMaterial> {
47    _marker: PhantomData<T>,
48}
49
50impl<T: FullscreenMaterial> Plugin for FullscreenMaterialPlugin<T> {
51    fn build(&self, app: &mut App) {
52        app.add_plugins((
53            ExtractComponentPlugin::<T>::default(),
54            UniformComponentPlugin::<T>::default(),
55        ));
56
57        let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
58            return;
59        };
60
61        render_app
62            .add_systems(RenderStartup, init_pipeline::<T>)
63            .add_systems(
64                Render,
65                (
66                    prepare_fullscreen_material_pipelines::<T>.in_set(RenderSystems::Prepare),
67                    prepare_bind_groups::<T>.in_set(RenderSystems::PrepareBindGroups),
68                ),
69            );
70
71        let system = T::schedule_configs(fullscreen_material_system::<T>.into_configs());
72        render_app.add_systems(T::schedule(), system);
73    }
74}
75
76/// A trait to define a material that will render to the entire screen using a fullscreen triangle.
77pub trait FullscreenMaterial:
78    Component + ExtractComponent + Clone + Copy + ShaderType + WriteInto + Default
79{
80    /// The shader that will run on the entire screen using a fullscreen triangle.
81    fn fragment_shader() -> ShaderRef;
82
83    /// The schedule this effect runs in.
84    ///
85    /// Defaults to [`Core3d`] for 3D post-processing effects.
86    fn schedule() -> impl ScheduleLabel + Clone {
87        Core3d
88    }
89
90    /// Configures this effect's system set and system order.
91    ///
92    /// By default it's in [`Core3dSystems::PostProcess`] and before [`tonemapping`].
93    fn schedule_configs(system: ScheduleConfigs<BoxedSystem>) -> ScheduleConfigs<BoxedSystem> {
94        system
95            .in_set(Core3dSystems::PostProcess)
96            .before(tonemapping)
97    }
98}
99
100#[derive(Resource)]
101pub struct FullscreenMaterialPipeline<T: FullscreenMaterial> {
102    layout: BindGroupLayoutDescriptor,
103    sampler: Sampler,
104    variants: Variants<RenderPipeline, FullscreenMaterialPipelineSpecializer>,
105    _marker: PhantomData<T>,
106}
107
108struct FullscreenMaterialPipelineSpecializer;
109
110#[derive(PartialEq, Eq, Hash, Clone, Copy, SpecializerKey)]
111struct FullscreenMaterialPipelineKey {
112    target_format: TextureFormat,
113}
114
115impl Specializer<RenderPipeline> for FullscreenMaterialPipelineSpecializer {
116    type Key = FullscreenMaterialPipelineKey;
117
118    fn specialize(
119        &self,
120        key: Self::Key,
121        descriptor: &mut RenderPipelineDescriptor,
122    ) -> Result<Canonical<Self::Key>, BevyError> {
123        let fragment = descriptor.fragment_mut()?;
124        let color_target_state = ColorTargetState {
125            format: key.target_format,
126            blend: None,
127            write_mask: ColorWrites::ALL,
128        };
129        fragment.set_target(0, color_target_state);
130        Ok(key)
131    }
132}
133
134fn init_pipeline<T: FullscreenMaterial>(
135    mut commands: Commands,
136    render_device: Res<RenderDevice>,
137    asset_server: Res<AssetServer>,
138    fullscreen_shader: Res<FullscreenShader>,
139) {
140    let layout = BindGroupLayoutDescriptor::new(
141        "fullscreen_material_bind_group_layout",
142        &BindGroupLayoutEntries::sequential(
143            ShaderStages::FRAGMENT,
144            (
145                texture_2d(TextureSampleType::Float { filterable: true }),
146                sampler(SamplerBindingType::Filtering),
147                uniform_buffer::<T>(true),
148            ),
149        ),
150    );
151    let sampler = render_device.create_sampler(&SamplerDescriptor::default());
152    let shader = match T::fragment_shader() {
153        ShaderRef::Default => {
154            unimplemented!(
155                "FullscreenMaterial::fragment_shader() must not return ShaderRef::Default"
156            )
157        }
158        ShaderRef::Handle(handle) => handle,
159        ShaderRef::Path(path) => asset_server.load(path),
160    };
161
162    let vertex_state = fullscreen_shader.to_vertex_state();
163    let desc = RenderPipelineDescriptor {
164        label: Some(format!("fullscreen_material_pipeline<{}>", type_name::<T>()).into()),
165        layout: vec![layout.clone()],
166        vertex: vertex_state,
167        fragment: Some(FragmentState {
168            shader,
169            targets: vec![Some(ColorTargetState {
170                format: TextureFormat::Rgba8UnormSrgb,
171                blend: None,
172                write_mask: ColorWrites::ALL,
173            })],
174            ..default()
175        }),
176        ..default()
177    };
178
179    commands.insert_resource(FullscreenMaterialPipeline::<T> {
180        layout,
181        sampler,
182        variants: Variants::new(FullscreenMaterialPipelineSpecializer, desc),
183        _marker: PhantomData,
184    });
185}
186
187#[derive(Component)]
188pub struct FullscreenMaterialPipelineId(pub CachedRenderPipelineId);
189
190fn prepare_fullscreen_material_pipelines<T: FullscreenMaterial>(
191    mut commands: Commands,
192    pipeline_cache: Res<PipelineCache>,
193    mut pipeline: ResMut<FullscreenMaterialPipeline<T>>,
194    views: Query<(Entity, &ExtractedView), With<ExtractedCamera>>,
195) -> Result<(), BevyError> {
196    for (entity, view) in &views {
197        let pipeline_key = FullscreenMaterialPipelineKey {
198            target_format: view.target_format,
199        };
200        let pipeline_id = pipeline
201            .variants
202            .specialize(&pipeline_cache, pipeline_key)?;
203
204        commands
205            .entity(entity)
206            .insert(FullscreenMaterialPipelineId(pipeline_id));
207    }
208
209    Ok(())
210}
211
212/// Holds the bind groups for both main textures
213///
214/// We can't know ahead of time which one is the source or destination so we create a bind group
215/// for both
216#[derive(Component)]
217pub struct FullscreenMaterialBindGroup<T: FullscreenMaterial> {
218    a: (TextureViewId, BindGroup),
219    b: (TextureViewId, BindGroup),
220    // This is in case someone wants multiple `FullscreenMaterial` per camera
221    _marker: PhantomData<T>,
222}
223
224/// Prepare the bind groups for both main textures for all views that have a [`FullscreenMaterial`]
225fn prepare_bind_groups<T: FullscreenMaterial>(
226    mut commands: Commands,
227    mut view: Query<(
228        Entity,
229        &ViewTarget,
230        Option<&mut FullscreenMaterialBindGroup<T>>,
231    )>,
232    fullscreen_pipeline: Option<Res<FullscreenMaterialPipeline<T>>>,
233    pipeline_cache: Res<PipelineCache>,
234    data_uniforms: Res<ComponentUniforms<T>>,
235    render_device: Res<RenderDevice>,
236) {
237    let Some(fullscreen_pipeline) = fullscreen_pipeline else {
238        return;
239    };
240    let Some(settings_binding) = data_uniforms.uniforms().binding() else {
241        return;
242    };
243
244    for (entity, view_target, mut maybe_bind_groups) in &mut view {
245        let main_texture_view = view_target.main_texture_view();
246        let main_texture_other_view = view_target.main_texture_other_view();
247
248        let create_bind_group = |texture: &TextureView| {
249            (
250                texture.id(),
251                render_device.create_bind_group(
252                    "fullscreen_material_bind_group",
253                    &pipeline_cache.get_bind_group_layout(&fullscreen_pipeline.layout),
254                    &BindGroupEntries::sequential((
255                        texture,
256                        &fullscreen_pipeline.sampler,
257                        settings_binding.clone(),
258                    )),
259                ),
260            )
261        };
262
263        if let Some(bind_groups) = &mut maybe_bind_groups {
264            if bind_groups.a.0 != main_texture_view.id() {
265                bind_groups.a = create_bind_group(main_texture_view);
266            }
267            if bind_groups.b.0 != main_texture_other_view.id() {
268                bind_groups.b = create_bind_group(main_texture_other_view);
269            }
270        } else {
271            commands.entity(entity).insert(FullscreenMaterialBindGroup {
272                a: create_bind_group(main_texture_view),
273                b: create_bind_group(main_texture_other_view),
274                _marker: PhantomData::<T>,
275            });
276        }
277    }
278}
279
280pub fn fullscreen_material_system<T: FullscreenMaterial>(
281    view: ViewQuery<(
282        &ViewTarget,
283        &DynamicUniformIndex<T>,
284        &FullscreenMaterialBindGroup<T>,
285        &FullscreenMaterialPipelineId,
286    )>,
287    pipeline_cache: Res<PipelineCache>,
288    mut ctx: RenderContext,
289) {
290    let (view_target, settings_index, bind_groups, pipeline_id) = view.into_inner();
291
292    let Some(pipeline) = pipeline_cache.get_render_pipeline(pipeline_id.0) else {
293        return;
294    };
295
296    let post_process = view_target.post_process_write();
297    let source = post_process.source;
298    let destination = post_process.destination;
299
300    let (_, bind_group) = if bind_groups.a.0 == source.id() {
301        &bind_groups.a
302    } else {
303        &bind_groups.b
304    };
305
306    let pass_descriptor = RenderPassDescriptor {
307        label: Some("fullscreen_material_pass"),
308        color_attachments: &[Some(RenderPassColorAttachment {
309            view: destination,
310            depth_slice: None,
311            resolve_target: None,
312            ops: Operations::default(),
313        })],
314        depth_stencil_attachment: None,
315        timestamp_writes: None,
316        occlusion_query_set: None,
317        multiview_mask: None,
318    };
319
320    {
321        let mut render_pass = ctx.command_encoder().begin_render_pass(&pass_descriptor);
322        render_pass.set_pipeline(pipeline);
323        render_pass.set_bind_group(0, bind_group, &[settings_index.index()]);
324        render_pass.draw(0..3, 0..1);
325    }
326}