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