Skip to main content

bevy_post_process/effect_stack/
mod.rs

1//! Miscellaneous built-in postprocessing effects.
2//!
3//! Includes:
4//!
5//! - Chromatic Aberration
6//! - Lens Distortion
7//! - Vignette
8
9mod chromatic_aberration;
10mod lens_distortion;
11mod vignette;
12
13use bevy_color::ColorToComponents;
14pub use chromatic_aberration::{ChromaticAberration, ChromaticAberrationUniform};
15pub use lens_distortion::{LensDistortion, LensDistortionUniform};
16pub use vignette::{Vignette, VignetteUniform};
17
18use crate::effect_stack::chromatic_aberration::{
19    DefaultChromaticAberrationLut, DEFAULT_CHROMATIC_ABERRATION_LUT_DATA,
20};
21
22use bevy_app::{App, Plugin};
23use bevy_asset::{
24    embedded_asset, load_embedded_asset, AssetServer, Assets, Handle, RenderAssetUsages,
25};
26use bevy_derive::{Deref, DerefMut};
27use bevy_ecs::{
28    component::Component,
29    entity::Entity,
30    query::{AnyOf, Or, With},
31    resource::Resource,
32    schedule::IntoScheduleConfigs as _,
33    system::{Commands, Query, Res, ResMut},
34};
35use bevy_image::Image;
36use bevy_render::{
37    camera::ExtractedCamera,
38    diagnostic::RecordDiagnostics,
39    extract_component::ExtractComponentPlugin,
40    render_asset::RenderAssets,
41    render_resource::{
42        binding_types::{sampler, texture_2d, uniform_buffer},
43        BindGroupEntries, BindGroupLayoutDescriptor, BindGroupLayoutEntries,
44        CachedRenderPipelineId, ColorTargetState, ColorWrites, DynamicUniformBuffer, Extent3d,
45        FilterMode, FragmentState, MipmapFilterMode, Operations, PipelineCache,
46        RenderPassColorAttachment, RenderPassDescriptor, RenderPipelineDescriptor, Sampler,
47        SamplerBindingType, SamplerDescriptor, ShaderStages, SpecializedRenderPipeline,
48        SpecializedRenderPipelines, TextureDimension, TextureFormat, TextureSampleType,
49    },
50    renderer::{RenderContext, RenderDevice, RenderQueue, ViewQuery},
51    texture::GpuImage,
52    view::{ExtractedView, ViewTarget},
53    GpuResourceAppExt, Render, RenderApp, RenderStartup, RenderSystems,
54};
55use bevy_shader::{load_shader_library, Shader};
56use bevy_utils::prelude::default;
57
58use crate::{bloom::bloom, dof::depth_of_field};
59use bevy_core_pipeline::{
60    schedule::{Core2d, Core3d},
61    tonemapping::tonemapping,
62    FullscreenShader,
63};
64
65/// A plugin that implements a built-in postprocessing stack with some common
66/// effects.
67///
68/// Includes:
69///
70/// - Chromatic Aberration
71/// - Lens Distortion
72/// - Vignette
73#[derive(Default)]
74pub struct EffectStackPlugin;
75
76/// GPU pipeline data for the built-in postprocessing stack.
77///
78/// This is stored in the render world.
79#[derive(Resource)]
80pub struct PostProcessingPipeline {
81    /// The layout of bind group 0, containing the source, LUT, and settings.
82    bind_group_layout: BindGroupLayoutDescriptor,
83    /// A shared sampler used to sample both the source framebuffer texture and the LUT texture.
84    common_sampler: Sampler,
85    /// The asset handle for the fullscreen vertex shader.
86    fullscreen_shader: FullscreenShader,
87    /// The fragment shader asset handle.
88    fragment_shader: Handle<Shader>,
89}
90
91/// A key that uniquely identifies a built-in postprocessing pipeline.
92#[derive(Clone, Copy, PartialEq, Eq, Hash)]
93pub struct PostProcessingPipelineKey {
94    /// The format of the source and destination textures.
95    target_format: TextureFormat,
96}
97
98/// A component attached to cameras in the render world that stores the
99/// specialized pipeline ID for the built-in postprocessing stack.
100#[derive(Component, Deref, DerefMut)]
101pub struct PostProcessingPipelineId(CachedRenderPipelineId);
102
103/// A resource, part of the render world, that stores the uniform buffers for
104/// post-processing effects.
105///
106/// This currently holds buffers for [`ChromaticAberrationUniform`] and
107/// [`VignetteUniform`], allowing them to be uploaded to the GPU efficiently.
108#[derive(Resource, Default)]
109pub struct PostProcessingUniformBuffers {
110    chromatic_aberration: DynamicUniformBuffer<ChromaticAberrationUniform>,
111    vignette: DynamicUniformBuffer<VignetteUniform>,
112    lens_distortion: DynamicUniformBuffer<LensDistortionUniform>,
113}
114
115/// A component, part of the render world, that stores the appropriate byte
116/// offset within the [`PostProcessingUniformBuffers`] for the camera it's
117/// attached to.
118#[derive(Component)]
119pub struct PostProcessingUniformBufferOffsets {
120    chromatic_aberration: u32,
121    vignette: u32,
122    lens_distortion: u32,
123}
124
125impl Plugin for EffectStackPlugin {
126    fn build(&self, app: &mut App) {
127        load_shader_library!(app, "chromatic_aberration.wgsl");
128        load_shader_library!(app, "lens_distortion.wgsl");
129        load_shader_library!(app, "vignette.wgsl");
130
131        embedded_asset!(app, "post_process.wgsl");
132
133        // Load the default chromatic aberration LUT.
134        let mut assets = app.world_mut().resource_mut::<Assets<_>>();
135        let default_lut = assets.add(Image::new(
136            Extent3d {
137                width: 3,
138                height: 1,
139                depth_or_array_layers: 1,
140            },
141            TextureDimension::D2,
142            DEFAULT_CHROMATIC_ABERRATION_LUT_DATA.to_vec(),
143            TextureFormat::Rgba8UnormSrgb,
144            RenderAssetUsages::RENDER_WORLD,
145        ));
146
147        app.add_plugins(ExtractComponentPlugin::<ChromaticAberration>::default())
148            .add_plugins(ExtractComponentPlugin::<LensDistortion>::default())
149            .add_plugins(ExtractComponentPlugin::<Vignette>::default());
150
151        let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
152            return;
153        };
154
155        render_app
156            .insert_resource(DefaultChromaticAberrationLut(default_lut))
157            .init_gpu_resource::<SpecializedRenderPipelines<PostProcessingPipeline>>()
158            .init_gpu_resource::<PostProcessingUniformBuffers>()
159            .add_systems(RenderStartup, init_post_processing_pipeline)
160            .add_systems(
161                Render,
162                (
163                    prepare_post_processing_pipelines,
164                    prepare_post_processing_uniforms,
165                )
166                    .in_set(RenderSystems::Prepare),
167            )
168            .add_systems(
169                Core3d,
170                post_processing.after(depth_of_field).before(tonemapping),
171            )
172            .add_systems(Core2d, post_processing.after(bloom).before(tonemapping));
173    }
174}
175
176pub(crate) fn init_post_processing_pipeline(
177    mut commands: Commands,
178    render_device: Res<RenderDevice>,
179    fullscreen_shader: Res<FullscreenShader>,
180    asset_server: Res<AssetServer>,
181) {
182    // Create our single bind group layout.
183    let bind_group_layout = BindGroupLayoutDescriptor::new(
184        "postprocessing bind group layout",
185        &BindGroupLayoutEntries::sequential(
186            ShaderStages::FRAGMENT,
187            (
188                // Common source:
189                texture_2d(TextureSampleType::Float { filterable: true }),
190                // Common sampler:
191                sampler(SamplerBindingType::Filtering),
192                // Chromatic aberration LUT:
193                texture_2d(TextureSampleType::Float { filterable: true }),
194                // Chromatic aberration settings:
195                uniform_buffer::<ChromaticAberrationUniform>(true),
196                // Vignette settings:
197                uniform_buffer::<VignetteUniform>(true),
198                // Lens Distortion settings:
199                uniform_buffer::<LensDistortionUniform>(true),
200            ),
201        ),
202    );
203
204    // Both source and chromatic aberration LUTs should be sampled
205    // bilinearly.
206    let common_sampler = render_device.create_sampler(&SamplerDescriptor {
207        mipmap_filter: MipmapFilterMode::Linear,
208        min_filter: FilterMode::Linear,
209        mag_filter: FilterMode::Linear,
210        ..default()
211    });
212
213    commands.insert_resource(PostProcessingPipeline {
214        bind_group_layout,
215        common_sampler,
216        fullscreen_shader: fullscreen_shader.clone(),
217        fragment_shader: load_embedded_asset!(asset_server.as_ref(), "post_process.wgsl"),
218    });
219}
220
221impl SpecializedRenderPipeline for PostProcessingPipeline {
222    type Key = PostProcessingPipelineKey;
223
224    fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
225        RenderPipelineDescriptor {
226            label: Some("postprocessing".into()),
227            layout: vec![self.bind_group_layout.clone()],
228            vertex: self.fullscreen_shader.to_vertex_state(),
229            fragment: Some(FragmentState {
230                shader: self.fragment_shader.clone(),
231                targets: vec![Some(ColorTargetState {
232                    format: key.target_format,
233                    blend: None,
234                    write_mask: ColorWrites::ALL,
235                })],
236                ..default()
237            }),
238            ..default()
239        }
240    }
241}
242
243pub(crate) fn post_processing(
244    view: ViewQuery<(
245        &ViewTarget,
246        &PostProcessingPipelineId,
247        AnyOf<(&ChromaticAberration, &Vignette, &LensDistortion)>,
248        &PostProcessingUniformBufferOffsets,
249    )>,
250    pipeline_cache: Res<PipelineCache>,
251    post_processing_pipeline: Res<PostProcessingPipeline>,
252    post_processing_uniform_buffers: Res<PostProcessingUniformBuffers>,
253    gpu_image_assets: Res<RenderAssets<GpuImage>>,
254    default_lut: Res<DefaultChromaticAberrationLut>,
255    mut ctx: RenderContext,
256) {
257    let (view_target, pipeline_id, post_effects, post_processing_uniform_buffer_offsets) =
258        view.into_inner();
259
260    let (maybe_chromatic_aberration, maybe_vignette, maybe_lens_distortion) = post_effects;
261
262    if maybe_chromatic_aberration.is_none()
263        && maybe_vignette.is_none()
264        && maybe_lens_distortion.is_none()
265    {
266        return;
267    }
268
269    // We need a render pipeline to be prepared.
270    let Some(pipeline) = pipeline_cache.get_render_pipeline(**pipeline_id) else {
271        return;
272    };
273
274    // We need the chromatic aberration LUT to be present.
275    let Some(chromatic_aberration_lut) = gpu_image_assets.get(
276        maybe_chromatic_aberration
277            .and_then(|ca| ca.color_lut.as_ref())
278            .unwrap_or(&default_lut.0),
279    ) else {
280        return;
281    };
282
283    // We need the postprocessing settings to be uploaded to the GPU.
284    let Some(chromatic_aberration_uniform_buffer_binding) = post_processing_uniform_buffers
285        .chromatic_aberration
286        .binding()
287    else {
288        return;
289    };
290
291    let Some(vignette_uniform_buffer_binding) = post_processing_uniform_buffers.vignette.binding()
292    else {
293        return;
294    };
295
296    let Some(lens_distortion_uniform_buffer_binding) =
297        post_processing_uniform_buffers.lens_distortion.binding()
298    else {
299        return;
300    };
301
302    // Use the [`PostProcessWrite`] infrastructure, since this is a full-screen pass.
303    let post_process = view_target.post_process_write();
304
305    let pass_descriptor = RenderPassDescriptor {
306        label: Some("postprocessing"),
307        color_attachments: &[Some(RenderPassColorAttachment {
308            view: post_process.destination,
309            depth_slice: None,
310            resolve_target: None,
311            ops: Operations::default(),
312        })],
313        depth_stencil_attachment: None,
314        timestamp_writes: None,
315        occlusion_query_set: None,
316        multiview_mask: None,
317    };
318
319    let bind_group = ctx.render_device().create_bind_group(
320        Some("postprocessing bind group"),
321        &pipeline_cache.get_bind_group_layout(&post_processing_pipeline.bind_group_layout),
322        &BindGroupEntries::sequential((
323            post_process.source,
324            &post_processing_pipeline.common_sampler,
325            &chromatic_aberration_lut.texture_view,
326            chromatic_aberration_uniform_buffer_binding,
327            vignette_uniform_buffer_binding,
328            lens_distortion_uniform_buffer_binding,
329        )),
330    );
331
332    let diagnostics = ctx.diagnostic_recorder();
333    let diagnostics = diagnostics.as_deref();
334
335    let mut render_pass = ctx.begin_tracked_render_pass(pass_descriptor);
336    let pass_span = diagnostics.pass_span(&mut render_pass, "postprocessing");
337
338    render_pass.set_render_pipeline(pipeline);
339    render_pass.set_bind_group(
340        0,
341        &bind_group,
342        &[
343            post_processing_uniform_buffer_offsets.chromatic_aberration,
344            post_processing_uniform_buffer_offsets.vignette,
345            post_processing_uniform_buffer_offsets.lens_distortion,
346        ],
347    );
348    render_pass.draw(0..3, 0..1);
349
350    pass_span.end(&mut render_pass);
351}
352
353/// Specializes the built-in postprocessing pipeline for each applicable view.
354pub(crate) fn prepare_post_processing_pipelines(
355    mut commands: Commands,
356    pipeline_cache: Res<PipelineCache>,
357    mut pipelines: ResMut<SpecializedRenderPipelines<PostProcessingPipeline>>,
358    post_processing_pipeline: Res<PostProcessingPipeline>,
359    cameras: Query<
360        (Entity, &ExtractedView),
361        Or<(
362            With<ChromaticAberration>,
363            With<Vignette>,
364            With<LensDistortion>,
365            With<ExtractedCamera>,
366        )>,
367    >,
368) {
369    for (entity, view) in cameras.iter() {
370        let pipeline_id = pipelines.specialize(
371            &pipeline_cache,
372            &post_processing_pipeline,
373            PostProcessingPipelineKey {
374                target_format: view.target_format,
375            },
376        );
377
378        commands
379            .entity(entity)
380            .insert(PostProcessingPipelineId(pipeline_id));
381    }
382}
383
384/// Gathers the built-in postprocessing settings for every view and uploads them
385/// to the GPU.
386pub(crate) fn prepare_post_processing_uniforms(
387    mut commands: Commands,
388    mut post_processing_uniform_buffers: ResMut<PostProcessingUniformBuffers>,
389    render_device: Res<RenderDevice>,
390    render_queue: Res<RenderQueue>,
391    mut views: Query<
392        (
393            Entity,
394            Option<&ChromaticAberration>,
395            Option<&Vignette>,
396            Option<&LensDistortion>,
397        ),
398        Or<(
399            With<ChromaticAberration>,
400            With<Vignette>,
401            With<LensDistortion>,
402        )>,
403    >,
404) {
405    post_processing_uniform_buffers.chromatic_aberration.clear();
406    post_processing_uniform_buffers.vignette.clear();
407    post_processing_uniform_buffers.lens_distortion.clear();
408
409    // Gather up all the postprocessing settings.
410    for (view_entity, maybe_chromatic_aberration, maybe_vignette, maybe_lens_distortion) in
411        views.iter_mut()
412    {
413        let chromatic_aberration_uniform_buffer_offset =
414            if let Some(chromatic_aberration) = maybe_chromatic_aberration {
415                post_processing_uniform_buffers.chromatic_aberration.push(
416                    &ChromaticAberrationUniform {
417                        intensity: chromatic_aberration.intensity,
418                        max_samples: chromatic_aberration.max_samples,
419                        unused_1: 0,
420                        unused_2: 0,
421                    },
422                )
423            } else {
424                post_processing_uniform_buffers
425                    .chromatic_aberration
426                    .push(&ChromaticAberrationUniform::default())
427            };
428
429        let vignette_uniform_buffer_offset = if let Some(vignette) = maybe_vignette {
430            post_processing_uniform_buffers
431                .vignette
432                .push(&VignetteUniform {
433                    intensity: vignette.intensity.min(1.0),
434                    radius: vignette.radius.max(1e-6),
435                    smoothness: vignette.smoothness.max(0.0),
436                    roundness: vignette.roundness.clamp(1e-6, 2.0 - 1e-6),
437                    center: vignette.center,
438                    edge_compensation: vignette.edge_compensation,
439                    unused: 0,
440                    color: vignette.color.to_srgba().to_vec4(),
441                })
442        } else {
443            post_processing_uniform_buffers
444                .vignette
445                .push(&VignetteUniform::default())
446        };
447
448        let lens_distortion_uniform_buffer_offset =
449            if let Some(lens_distortion) = maybe_lens_distortion {
450                post_processing_uniform_buffers
451                    .lens_distortion
452                    .push(&LensDistortionUniform {
453                        intensity: lens_distortion.intensity,
454                        scale: lens_distortion.scale.max(1e-6),
455                        multiplier: lens_distortion.multiplier,
456                        center: lens_distortion.center,
457                        edge_curvature: lens_distortion.edge_curvature,
458                        unused: 0,
459                    })
460            } else {
461                post_processing_uniform_buffers
462                    .lens_distortion
463                    .push(&LensDistortionUniform::default())
464            };
465
466        commands
467            .entity(view_entity)
468            .insert(PostProcessingUniformBufferOffsets {
469                chromatic_aberration: chromatic_aberration_uniform_buffer_offset,
470                vignette: vignette_uniform_buffer_offset,
471                lens_distortion: lens_distortion_uniform_buffer_offset,
472            });
473    }
474
475    // Upload to the GPU.
476    post_processing_uniform_buffers
477        .chromatic_aberration
478        .write_buffer(&render_device, &render_queue);
479    post_processing_uniform_buffers
480        .vignette
481        .write_buffer(&render_device, &render_queue);
482    post_processing_uniform_buffers
483        .lens_distortion
484        .write_buffer(&render_device, &render_queue);
485}