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