Skip to main content

custom_post_processing/
custom_post_processing.rs

1//! This example shows how to create a custom post-processing effect that runs after the main pass
2//! and reads the texture generated by the main pass.
3//!
4//! The example shader is a very simple implementation of chromatic aberration.
5//! To adapt this example for 2D, replace all instances of 3D structures (such as `Core3d`, etc.) with their corresponding 2D counterparts.
6//!
7//! This is a fairly low level example and assumes some familiarity with rendering concepts and wgpu.
8
9use bevy::{
10    core_pipeline::{schedule::Core3d, Core3dSystems, FullscreenShader},
11    prelude::*,
12    render::{
13        extract_component::{
14            ComponentUniforms, DynamicUniformIndex, ExtractComponent, ExtractComponentPlugin,
15            UniformComponentPlugin,
16        },
17        render_resource::{
18            binding_types::{sampler, texture_2d, uniform_buffer},
19            *,
20        },
21        renderer::{RenderContext, RenderDevice, ViewQuery},
22        view::ViewTarget,
23        RenderApp, RenderStartup,
24    },
25};
26
27/// This example uses a shader source file from the assets subdirectory
28const SHADER_ASSET_PATH: &str = "shaders/post_processing.wgsl";
29
30fn main() {
31    App::new()
32        .add_plugins((DefaultPlugins, PostProcessPlugin))
33        .add_systems(Startup, setup)
34        .add_systems(Update, (rotate, update_settings))
35        .run();
36}
37
38/// It is generally encouraged to set up post processing effects as a plugin
39struct PostProcessPlugin;
40
41impl Plugin for PostProcessPlugin {
42    fn build(&self, app: &mut App) {
43        app.add_plugins((
44            // The settings will be a component that lives in the main world but will
45            // be extracted to the render world every frame.
46            // This makes it possible to control the effect from the main world.
47            // This plugin will take care of extracting it automatically.
48            // It's important to derive [`ExtractComponent`] on [`PostProcessSettings`]
49            // for this plugin to work correctly.
50            ExtractComponentPlugin::<PostProcessSettings>::default(),
51            // The settings will also be the data used in the shader.
52            // This plugin will prepare the component for the GPU by creating a uniform buffer
53            // and writing the data to that buffer every frame.
54            UniformComponentPlugin::<PostProcessSettings>::default(),
55        ));
56
57        // We need to get the render app from the main app
58        let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
59            return;
60        };
61
62        render_app.add_systems(RenderStartup, init_post_process_pipeline);
63        render_app.add_systems(
64            Core3d,
65            post_process_system.in_set(Core3dSystems::PostProcess),
66        );
67    }
68}
69
70#[derive(Default)]
71struct PostProcessBindGroupCache {
72    cached: Option<(TextureViewId, BindGroup)>,
73}
74
75fn post_process_system(
76    view: ViewQuery<(
77        &ViewTarget,
78        &PostProcessSettings,
79        &DynamicUniformIndex<PostProcessSettings>,
80    )>,
81    post_process_pipeline: Option<Res<PostProcessPipeline>>,
82    pipeline_cache: Res<PipelineCache>,
83    settings_uniforms: Res<ComponentUniforms<PostProcessSettings>>,
84    mut cache: Local<PostProcessBindGroupCache>,
85    mut ctx: RenderContext,
86) {
87    let Some(post_process_pipeline) = post_process_pipeline else {
88        return;
89    };
90
91    let (view_target, _post_process_settings, settings_index) = view.into_inner();
92
93    let Some(pipeline) = pipeline_cache.get_render_pipeline(post_process_pipeline.pipeline_id)
94    else {
95        return;
96    };
97
98    let Some(settings_binding) = settings_uniforms.uniforms().binding() else {
99        return;
100    };
101
102    // This will start a new "post process write", obtaining two texture
103    // views from the view target - a `source` and a `destination`.
104    // `source` is the "current" main texture and you _must_ write into
105    // `destination` because calling `post_process_write()` on the
106    // [`ViewTarget`] will internally flip the [`ViewTarget`]'s main
107    // texture to the `destination` texture. Failing to do so will cause
108    // the current main texture information to be lost.
109    let post_process = view_target.post_process_write();
110
111    let bind_group = match &mut cache.cached {
112        Some((texture_id, bind_group)) if post_process.source.id() == *texture_id => bind_group,
113        cached => {
114            // The bind_group gets created each frame.
115            //
116            // Normally, you would create a bind_group in the Queue set,
117            // but this doesn't work with the post_process_write().
118            // The reason it doesn't work is because each post_process_write will alternate the source/destination.
119            // The only way to have the correct source/destination for the bind_group
120            // is to make sure you get it during the node execution.
121            let bind_group = ctx.render_device().create_bind_group(
122                "post_process_bind_group",
123                &pipeline_cache.get_bind_group_layout(&post_process_pipeline.layout),
124                // It's important for this to match the BindGroupLayout defined in the PostProcessPipeline
125                &BindGroupEntries::sequential((
126                    // Make sure to use the source view
127                    post_process.source,
128                    // Use the sampler created for the pipeline
129                    &post_process_pipeline.sampler,
130                    // Set the settings binding
131                    settings_binding.clone(),
132                )),
133            );
134
135            let (_, bind_group) = cached.insert((post_process.source.id(), bind_group));
136            bind_group
137        }
138    };
139
140    let mut render_pass = ctx
141        .command_encoder()
142        .begin_render_pass(&RenderPassDescriptor {
143            label: Some("post_process_pass"),
144            color_attachments: &[Some(RenderPassColorAttachment {
145                // We need to specify the post process destination view here
146                // to make sure we write to the appropriate texture.
147                view: post_process.destination,
148                depth_slice: None,
149                resolve_target: None,
150                ops: Operations::default(),
151            })],
152            depth_stencil_attachment: None,
153            timestamp_writes: None,
154            occlusion_query_set: None,
155            multiview_mask: None,
156        });
157
158    render_pass.set_pipeline(pipeline);
159    // By passing in the index of the post process settings on this view, we ensure
160    // that in the event that multiple settings were sent to the GPU (as would be the
161    // case with multiple cameras), we use the correct one.
162    render_pass.set_bind_group(0, bind_group, &[settings_index.index()]);
163    render_pass.draw(0..3, 0..1);
164}
165
166// This contains global data used by the render pipeline. This will be created once on startup.
167#[derive(Resource)]
168struct PostProcessPipeline {
169    layout: BindGroupLayoutDescriptor,
170    sampler: Sampler,
171    pipeline_id: CachedRenderPipelineId,
172}
173
174fn init_post_process_pipeline(
175    mut commands: Commands,
176    render_device: Res<RenderDevice>,
177    asset_server: Res<AssetServer>,
178    fullscreen_shader: Res<FullscreenShader>,
179    pipeline_cache: Res<PipelineCache>,
180) {
181    // We need to define the bind group layout used for our pipeline
182    let layout = BindGroupLayoutDescriptor::new(
183        "post_process_bind_group_layout",
184        &BindGroupLayoutEntries::sequential(
185            // The layout entries will only be visible in the fragment stage
186            ShaderStages::FRAGMENT,
187            (
188                // The screen texture
189                texture_2d(TextureSampleType::Float { filterable: true }),
190                // The sampler that will be used to sample the screen texture
191                sampler(SamplerBindingType::Filtering),
192                // The settings uniform that will control the effect
193                uniform_buffer::<PostProcessSettings>(true),
194            ),
195        ),
196    );
197    // We can create the sampler here since it won't change at runtime and doesn't depend on the view
198    let sampler = render_device.create_sampler(&SamplerDescriptor::default());
199
200    // Get the shader handle
201    let shader = asset_server.load(SHADER_ASSET_PATH);
202    // This will setup a fullscreen triangle for the vertex state.
203    let vertex_state = fullscreen_shader.to_vertex_state();
204    let pipeline_id = pipeline_cache
205        // This will add the pipeline to the cache and queue its creation
206        .queue_render_pipeline(RenderPipelineDescriptor {
207            label: Some("post_process_pipeline".into()),
208            layout: vec![layout.clone()],
209            vertex: vertex_state,
210            fragment: Some(FragmentState {
211                shader,
212                // Make sure this matches the entry point of your shader.
213                // It can be anything as long as it matches here and in the shader.
214                targets: vec![Some(ColorTargetState {
215                    format: TextureFormat::Rgba8UnormSrgb,
216                    blend: None,
217                    write_mask: ColorWrites::ALL,
218                })],
219                ..default()
220            }),
221            ..default()
222        });
223    commands.insert_resource(PostProcessPipeline {
224        layout,
225        sampler,
226        pipeline_id,
227    });
228}
229
230// This is the component that will get passed to the shader
231#[derive(Component, Default, Clone, Copy, ExtractComponent, ShaderType)]
232struct PostProcessSettings {
233    intensity: f32,
234    // WebGL2 structs must be 16 byte aligned.
235    #[cfg(feature = "webgl2")]
236    _webgl2_padding: Vec3,
237}
238
239/// Set up a simple 3D scene
240fn setup(
241    mut commands: Commands,
242    mut meshes: ResMut<Assets<Mesh>>,
243    mut materials: ResMut<Assets<StandardMaterial>>,
244) {
245    // camera
246    // Make sure you change the TextureFormat of the ColorTargetState
247    // if you enable Hdr directly or through features like Bloom.
248    commands.spawn((
249        Camera3d::default(),
250        Transform::from_translation(Vec3::new(0.0, 0.0, 5.0)).looking_at(Vec3::default(), Vec3::Y),
251        Camera {
252            clear_color: Color::WHITE.into(),
253            ..default()
254        },
255        // Add the setting to the camera.
256        // This component is also used to determine on which camera to run the post processing effect.
257        PostProcessSettings {
258            intensity: 0.02,
259            ..default()
260        },
261    ));
262
263    // cube
264    commands.spawn((
265        Mesh3d(meshes.add(Cuboid::default())),
266        MeshMaterial3d(materials.add(Color::srgb(0.8, 0.7, 0.6))),
267        Transform::from_xyz(0.0, 0.5, 0.0),
268        Rotates,
269    ));
270    // light
271    commands.spawn(DirectionalLight {
272        illuminance: 1_000.,
273        ..default()
274    });
275}
276
277#[derive(Component)]
278struct Rotates;
279
280/// Rotates any entity around the x and y axis
281fn rotate(time: Res<Time>, mut query: Query<&mut Transform, With<Rotates>>) {
282    for mut transform in &mut query {
283        transform.rotate_x(0.55 * time.delta_secs());
284        transform.rotate_z(0.15 * time.delta_secs());
285    }
286}
287
288// Change the intensity over time to show that the effect is controlled from the main world
289fn update_settings(mut settings: Query<&mut PostProcessSettings>, time: Res<Time>) {
290    for mut setting in &mut settings {
291        let mut intensity = ops::sin(time.elapsed_secs());
292        // Make it loop periodically
293        intensity = ops::sin(intensity);
294        // Remap it to 0..1 because the intensity can't be negative
295        intensity = intensity * 0.5 + 0.5;
296        // Scale it to a more reasonable level
297        intensity *= 0.015;
298
299        // Set the intensity.
300        // This will then be extracted to the render world and uploaded to the GPU automatically by the [`UniformComponentPlugin`]
301        setting.intensity = intensity;
302    }
303}