bevy_post_process/effect_stack/
mod.rs

1//! Miscellaneous built-in postprocessing effects.
2//!
3//! Currently, this consists only of chromatic aberration.
4
5use bevy_app::{App, Plugin};
6use bevy_asset::{
7    embedded_asset, load_embedded_asset, AssetServer, Assets, Handle, RenderAssetUsages,
8};
9use bevy_camera::Camera;
10use bevy_derive::{Deref, DerefMut};
11use bevy_ecs::{
12    component::Component,
13    entity::Entity,
14    query::{QueryItem, With},
15    reflect::ReflectComponent,
16    resource::Resource,
17    schedule::IntoScheduleConfigs as _,
18    system::{lifetimeless::Read, Commands, Query, Res, ResMut},
19    world::World,
20};
21use bevy_image::{BevyDefault, Image};
22use bevy_reflect::{std_traits::ReflectDefault, Reflect};
23use bevy_render::{
24    diagnostic::RecordDiagnostics,
25    extract_component::{ExtractComponent, ExtractComponentPlugin},
26    render_asset::RenderAssets,
27    render_graph::{
28        NodeRunError, RenderGraphContext, RenderGraphExt as _, ViewNode, ViewNodeRunner,
29    },
30    render_resource::{
31        binding_types::{sampler, texture_2d, uniform_buffer},
32        BindGroupEntries, BindGroupLayout, BindGroupLayoutEntries, CachedRenderPipelineId,
33        ColorTargetState, ColorWrites, DynamicUniformBuffer, Extent3d, FilterMode, FragmentState,
34        Operations, PipelineCache, RenderPassColorAttachment, RenderPassDescriptor,
35        RenderPipelineDescriptor, Sampler, SamplerBindingType, SamplerDescriptor, ShaderStages,
36        ShaderType, SpecializedRenderPipeline, SpecializedRenderPipelines, TextureDimension,
37        TextureFormat, TextureSampleType,
38    },
39    renderer::{RenderContext, RenderDevice, RenderQueue},
40    texture::GpuImage,
41    view::{ExtractedView, ViewTarget},
42    Render, RenderApp, RenderStartup, RenderSystems,
43};
44use bevy_shader::{load_shader_library, Shader};
45use bevy_utils::prelude::default;
46
47use bevy_core_pipeline::{
48    core_2d::graph::{Core2d, Node2d},
49    core_3d::graph::{Core3d, Node3d},
50    FullscreenShader,
51};
52
53/// The default chromatic aberration intensity amount, in a fraction of the
54/// window size.
55const DEFAULT_CHROMATIC_ABERRATION_INTENSITY: f32 = 0.02;
56
57/// The default maximum number of samples for chromatic aberration.
58const DEFAULT_CHROMATIC_ABERRATION_MAX_SAMPLES: u32 = 8;
59
60/// The raw RGBA data for the default chromatic aberration gradient.
61///
62/// This consists of one red pixel, one green pixel, and one blue pixel, in that
63/// order.
64static DEFAULT_CHROMATIC_ABERRATION_LUT_DATA: [u8; 12] =
65    [255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255];
66
67#[derive(Resource)]
68struct DefaultChromaticAberrationLut(Handle<Image>);
69
70/// A plugin that implements a built-in postprocessing stack with some common
71/// effects.
72///
73/// Currently, this only consists of chromatic aberration.
74#[derive(Default)]
75pub struct EffectStackPlugin;
76
77/// Adds colored fringes to the edges of objects in the scene.
78///
79/// [Chromatic aberration] simulates the effect when lenses fail to focus all
80/// colors of light toward a single point. It causes rainbow-colored streaks to
81/// appear, which are especially apparent on the edges of objects. Chromatic
82/// aberration is commonly used for collision effects, especially in horror
83/// games.
84///
85/// Bevy's implementation is based on that of *Inside* ([Gjøl & Svendsen 2016]).
86/// It's based on a customizable lookup texture, which allows for changing the
87/// color pattern. By default, the color pattern is simply a 3×1 pixel texture
88/// consisting of red, green, and blue, in that order, but you can change it to
89/// any image in order to achieve different effects.
90///
91/// [Chromatic aberration]: https://en.wikipedia.org/wiki/Chromatic_aberration
92///
93/// [Gjøl & Svendsen 2016]: https://github.com/playdeadgames/publications/blob/master/INSIDE/rendering_inside_gdc2016.pdf
94#[derive(Reflect, Component, Clone)]
95#[reflect(Component, Default, Clone)]
96pub struct ChromaticAberration {
97    /// The lookup texture that determines the color gradient.
98    ///
99    /// By default (if None), this is a 3×1 texel texture consisting of one red
100    /// pixel, one green pixel, and one blue texel, in that order. This
101    /// recreates the most typical chromatic aberration pattern. However, you
102    /// can change it to achieve different artistic effects.
103    ///
104    /// The texture is always sampled in its vertical center, so it should
105    /// ordinarily have a height of 1 texel.
106    pub color_lut: Option<Handle<Image>>,
107
108    /// The size of the streaks around the edges of objects, as a fraction of
109    /// the window size.
110    ///
111    /// The default value is 0.02.
112    pub intensity: f32,
113
114    /// A cap on the number of texture samples that will be performed.
115    ///
116    /// Higher values result in smoother-looking streaks but are slower.
117    ///
118    /// The default value is 8.
119    pub max_samples: u32,
120}
121
122/// GPU pipeline data for the built-in postprocessing stack.
123///
124/// This is stored in the render world.
125#[derive(Resource)]
126pub struct PostProcessingPipeline {
127    /// The layout of bind group 0, containing the source, LUT, and settings.
128    bind_group_layout: BindGroupLayout,
129    /// Specifies how to sample the source framebuffer texture.
130    source_sampler: Sampler,
131    /// Specifies how to sample the chromatic aberration gradient.
132    chromatic_aberration_lut_sampler: Sampler,
133    /// The asset handle for the fullscreen vertex shader.
134    fullscreen_shader: FullscreenShader,
135    /// The fragment shader asset handle.
136    fragment_shader: Handle<Shader>,
137}
138
139/// A key that uniquely identifies a built-in postprocessing pipeline.
140#[derive(Clone, Copy, PartialEq, Eq, Hash)]
141pub struct PostProcessingPipelineKey {
142    /// The format of the source and destination textures.
143    texture_format: TextureFormat,
144}
145
146/// A component attached to cameras in the render world that stores the
147/// specialized pipeline ID for the built-in postprocessing stack.
148#[derive(Component, Deref, DerefMut)]
149pub struct PostProcessingPipelineId(CachedRenderPipelineId);
150
151/// The on-GPU version of the [`ChromaticAberration`] settings.
152///
153/// See the documentation for [`ChromaticAberration`] for more information on
154/// each of these fields.
155#[derive(ShaderType)]
156pub struct ChromaticAberrationUniform {
157    /// The intensity of the effect, in a fraction of the screen.
158    intensity: f32,
159    /// A cap on the number of samples of the source texture that the shader
160    /// will perform.
161    max_samples: u32,
162    /// Padding data.
163    unused_1: u32,
164    /// Padding data.
165    unused_2: u32,
166}
167
168/// A resource, part of the render world, that stores the
169/// [`ChromaticAberrationUniform`]s for each view.
170#[derive(Resource, Deref, DerefMut, Default)]
171pub struct PostProcessingUniformBuffers {
172    chromatic_aberration: DynamicUniformBuffer<ChromaticAberrationUniform>,
173}
174
175/// A component, part of the render world, that stores the appropriate byte
176/// offset within the [`PostProcessingUniformBuffers`] for the camera it's
177/// attached to.
178#[derive(Component, Deref, DerefMut)]
179pub struct PostProcessingUniformBufferOffsets {
180    chromatic_aberration: u32,
181}
182
183/// The render node that runs the built-in postprocessing stack.
184#[derive(Default)]
185pub struct PostProcessingNode;
186
187impl Plugin for EffectStackPlugin {
188    fn build(&self, app: &mut App) {
189        load_shader_library!(app, "chromatic_aberration.wgsl");
190
191        embedded_asset!(app, "post_process.wgsl");
192
193        // Load the default chromatic aberration LUT.
194        let mut assets = app.world_mut().resource_mut::<Assets<_>>();
195        let default_lut = assets.add(Image::new(
196            Extent3d {
197                width: 3,
198                height: 1,
199                depth_or_array_layers: 1,
200            },
201            TextureDimension::D2,
202            DEFAULT_CHROMATIC_ABERRATION_LUT_DATA.to_vec(),
203            TextureFormat::Rgba8UnormSrgb,
204            RenderAssetUsages::RENDER_WORLD,
205        ));
206
207        app.add_plugins(ExtractComponentPlugin::<ChromaticAberration>::default());
208
209        let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
210            return;
211        };
212
213        render_app
214            .insert_resource(DefaultChromaticAberrationLut(default_lut))
215            .init_resource::<SpecializedRenderPipelines<PostProcessingPipeline>>()
216            .init_resource::<PostProcessingUniformBuffers>()
217            .add_systems(RenderStartup, init_post_processing_pipeline)
218            .add_systems(
219                Render,
220                (
221                    prepare_post_processing_pipelines,
222                    prepare_post_processing_uniforms,
223                )
224                    .in_set(RenderSystems::Prepare),
225            )
226            .add_render_graph_node::<ViewNodeRunner<PostProcessingNode>>(
227                Core3d,
228                Node3d::PostProcessing,
229            )
230            .add_render_graph_edges(
231                Core3d,
232                (
233                    Node3d::DepthOfField,
234                    Node3d::PostProcessing,
235                    Node3d::Tonemapping,
236                ),
237            )
238            .add_render_graph_node::<ViewNodeRunner<PostProcessingNode>>(
239                Core2d,
240                Node2d::PostProcessing,
241            )
242            .add_render_graph_edges(
243                Core2d,
244                (Node2d::Bloom, Node2d::PostProcessing, Node2d::Tonemapping),
245            );
246    }
247}
248
249impl Default for ChromaticAberration {
250    fn default() -> Self {
251        Self {
252            color_lut: None,
253            intensity: DEFAULT_CHROMATIC_ABERRATION_INTENSITY,
254            max_samples: DEFAULT_CHROMATIC_ABERRATION_MAX_SAMPLES,
255        }
256    }
257}
258
259pub fn init_post_processing_pipeline(
260    mut commands: Commands,
261    render_device: Res<RenderDevice>,
262    fullscreen_shader: Res<FullscreenShader>,
263    asset_server: Res<AssetServer>,
264) {
265    // Create our single bind group layout.
266    let bind_group_layout = render_device.create_bind_group_layout(
267        Some("postprocessing bind group layout"),
268        &BindGroupLayoutEntries::sequential(
269            ShaderStages::FRAGMENT,
270            (
271                // Chromatic aberration source:
272                texture_2d(TextureSampleType::Float { filterable: true }),
273                // Chromatic aberration source sampler:
274                sampler(SamplerBindingType::Filtering),
275                // Chromatic aberration LUT:
276                texture_2d(TextureSampleType::Float { filterable: true }),
277                // Chromatic aberration LUT sampler:
278                sampler(SamplerBindingType::Filtering),
279                // Chromatic aberration settings:
280                uniform_buffer::<ChromaticAberrationUniform>(true),
281            ),
282        ),
283    );
284
285    // Both source and chromatic aberration LUTs should be sampled
286    // bilinearly.
287
288    let source_sampler = render_device.create_sampler(&SamplerDescriptor {
289        mipmap_filter: FilterMode::Linear,
290        min_filter: FilterMode::Linear,
291        mag_filter: FilterMode::Linear,
292        ..default()
293    });
294
295    let chromatic_aberration_lut_sampler = render_device.create_sampler(&SamplerDescriptor {
296        mipmap_filter: FilterMode::Linear,
297        min_filter: FilterMode::Linear,
298        mag_filter: FilterMode::Linear,
299        ..default()
300    });
301
302    commands.insert_resource(PostProcessingPipeline {
303        bind_group_layout,
304        source_sampler,
305        chromatic_aberration_lut_sampler,
306        fullscreen_shader: fullscreen_shader.clone(),
307        fragment_shader: load_embedded_asset!(asset_server.as_ref(), "post_process.wgsl"),
308    });
309}
310
311impl SpecializedRenderPipeline for PostProcessingPipeline {
312    type Key = PostProcessingPipelineKey;
313
314    fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
315        RenderPipelineDescriptor {
316            label: Some("postprocessing".into()),
317            layout: vec![self.bind_group_layout.clone()],
318            vertex: self.fullscreen_shader.to_vertex_state(),
319            fragment: Some(FragmentState {
320                shader: self.fragment_shader.clone(),
321                targets: vec![Some(ColorTargetState {
322                    format: key.texture_format,
323                    blend: None,
324                    write_mask: ColorWrites::ALL,
325                })],
326                ..default()
327            }),
328            ..default()
329        }
330    }
331}
332
333impl ViewNode for PostProcessingNode {
334    type ViewQuery = (
335        Read<ViewTarget>,
336        Read<PostProcessingPipelineId>,
337        Read<ChromaticAberration>,
338        Read<PostProcessingUniformBufferOffsets>,
339    );
340
341    fn run<'w>(
342        &self,
343        _: &mut RenderGraphContext,
344        render_context: &mut RenderContext<'w>,
345        (view_target, pipeline_id, chromatic_aberration, post_processing_uniform_buffer_offsets): QueryItem<'w, '_, Self::ViewQuery>,
346        world: &'w World,
347    ) -> Result<(), NodeRunError> {
348        let pipeline_cache = world.resource::<PipelineCache>();
349        let post_processing_pipeline = world.resource::<PostProcessingPipeline>();
350        let post_processing_uniform_buffers = world.resource::<PostProcessingUniformBuffers>();
351        let gpu_image_assets = world.resource::<RenderAssets<GpuImage>>();
352        let default_lut = world.resource::<DefaultChromaticAberrationLut>();
353
354        // We need a render pipeline to be prepared.
355        let Some(pipeline) = pipeline_cache.get_render_pipeline(**pipeline_id) else {
356            return Ok(());
357        };
358
359        // We need the chromatic aberration LUT to be present.
360        let Some(chromatic_aberration_lut) = gpu_image_assets.get(
361            chromatic_aberration
362                .color_lut
363                .as_ref()
364                .unwrap_or(&default_lut.0),
365        ) else {
366            return Ok(());
367        };
368
369        // We need the postprocessing settings to be uploaded to the GPU.
370        let Some(chromatic_aberration_uniform_buffer_binding) = post_processing_uniform_buffers
371            .chromatic_aberration
372            .binding()
373        else {
374            return Ok(());
375        };
376
377        let diagnostics = render_context.diagnostic_recorder();
378
379        // Use the [`PostProcessWrite`] infrastructure, since this is a
380        // full-screen pass.
381        let post_process = view_target.post_process_write();
382
383        let pass_descriptor = RenderPassDescriptor {
384            label: Some("postprocessing"),
385            color_attachments: &[Some(RenderPassColorAttachment {
386                view: post_process.destination,
387                depth_slice: None,
388                resolve_target: None,
389                ops: Operations::default(),
390            })],
391            depth_stencil_attachment: None,
392            timestamp_writes: None,
393            occlusion_query_set: None,
394        };
395
396        let bind_group = render_context.render_device().create_bind_group(
397            Some("postprocessing bind group"),
398            &post_processing_pipeline.bind_group_layout,
399            &BindGroupEntries::sequential((
400                post_process.source,
401                &post_processing_pipeline.source_sampler,
402                &chromatic_aberration_lut.texture_view,
403                &post_processing_pipeline.chromatic_aberration_lut_sampler,
404                chromatic_aberration_uniform_buffer_binding,
405            )),
406        );
407
408        let mut render_pass = render_context
409            .command_encoder()
410            .begin_render_pass(&pass_descriptor);
411        let pass_span = diagnostics.pass_span(&mut render_pass, "postprocessing");
412
413        render_pass.set_pipeline(pipeline);
414        render_pass.set_bind_group(0, &bind_group, &[**post_processing_uniform_buffer_offsets]);
415        render_pass.draw(0..3, 0..1);
416
417        pass_span.end(&mut render_pass);
418
419        Ok(())
420    }
421}
422
423/// Specializes the built-in postprocessing pipeline for each applicable view.
424pub fn prepare_post_processing_pipelines(
425    mut commands: Commands,
426    pipeline_cache: Res<PipelineCache>,
427    mut pipelines: ResMut<SpecializedRenderPipelines<PostProcessingPipeline>>,
428    post_processing_pipeline: Res<PostProcessingPipeline>,
429    views: Query<(Entity, &ExtractedView), With<ChromaticAberration>>,
430) {
431    for (entity, view) in views.iter() {
432        let pipeline_id = pipelines.specialize(
433            &pipeline_cache,
434            &post_processing_pipeline,
435            PostProcessingPipelineKey {
436                texture_format: if view.hdr {
437                    ViewTarget::TEXTURE_FORMAT_HDR
438                } else {
439                    TextureFormat::bevy_default()
440                },
441            },
442        );
443
444        commands
445            .entity(entity)
446            .insert(PostProcessingPipelineId(pipeline_id));
447    }
448}
449
450/// Gathers the built-in postprocessing settings for every view and uploads them
451/// to the GPU.
452pub fn prepare_post_processing_uniforms(
453    mut commands: Commands,
454    mut post_processing_uniform_buffers: ResMut<PostProcessingUniformBuffers>,
455    render_device: Res<RenderDevice>,
456    render_queue: Res<RenderQueue>,
457    mut views: Query<(Entity, &ChromaticAberration)>,
458) {
459    post_processing_uniform_buffers.clear();
460
461    // Gather up all the postprocessing settings.
462    for (view_entity, chromatic_aberration) in views.iter_mut() {
463        let chromatic_aberration_uniform_buffer_offset =
464            post_processing_uniform_buffers.push(&ChromaticAberrationUniform {
465                intensity: chromatic_aberration.intensity,
466                max_samples: chromatic_aberration.max_samples,
467                unused_1: 0,
468                unused_2: 0,
469            });
470        commands
471            .entity(view_entity)
472            .insert(PostProcessingUniformBufferOffsets {
473                chromatic_aberration: chromatic_aberration_uniform_buffer_offset,
474            });
475    }
476
477    // Upload to the GPU.
478    post_processing_uniform_buffers.write_buffer(&render_device, &render_queue);
479}
480
481impl ExtractComponent for ChromaticAberration {
482    type QueryData = Read<ChromaticAberration>;
483
484    type QueryFilter = With<Camera>;
485
486    type Out = ChromaticAberration;
487
488    fn extract_component(
489        chromatic_aberration: QueryItem<'_, '_, Self::QueryData>,
490    ) -> Option<Self::Out> {
491        // Skip the postprocessing phase entirely if the intensity is zero.
492        if chromatic_aberration.intensity > 0.0 {
493            Some(chromatic_aberration.clone())
494        } else {
495            None
496        }
497    }
498}