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