//! This example shows how to create a custom post-processing effect that runs after the main pass //! and reads the texture generated by the main pass. //! //! The example shader is a very simple implementation of chromatic aberration. //! To adapt this example for 2D, replace all instances of 3D structures (such as `Core3d`, etc.) with their corresponding 2D counterparts. //! //! This is a fairly low level example and assumes some familiarity with rendering concepts and wgpu. use bevy::{ core_pipeline::{schedule::Core3d, Core3dSystems, FullscreenShader}, prelude::*, render::{ extract_component::{ ComponentUniforms, DynamicUniformIndex, ExtractComponent, ExtractComponentPlugin, UniformComponentPlugin, }, render_resource::{ binding_types::{sampler, texture_2d, uniform_buffer}, *, }, renderer::{RenderContext, RenderDevice, ViewQuery}, view::ViewTarget, RenderApp, RenderStartup, }, }; /// This example uses a shader source file from the assets subdirectory const SHADER_ASSET_PATH: &str = "shaders/post_processing.wgsl"; fn main() { App::new() .add_plugins((DefaultPlugins, PostProcessPlugin)) .add_systems(Startup, setup) .add_systems(Update, (rotate, update_settings)) .run(); } /// It is generally encouraged to set up post processing effects as a plugin struct PostProcessPlugin; impl Plugin for PostProcessPlugin { fn build(&self, app: &mut App) { app.add_plugins(( // The settings will be a component that lives in the main world but will // be extracted to the render world every frame. // This makes it possible to control the effect from the main world. // This plugin will take care of extracting it automatically. // It's important to derive [`ExtractComponent`] on [`PostProcessSettings`] // for this plugin to work correctly. ExtractComponentPlugin::::default(), // The settings will also be the data used in the shader. // This plugin will prepare the component for the GPU by creating a uniform buffer // and writing the data to that buffer every frame. UniformComponentPlugin::::default(), )); // We need to get the render app from the main app let Some(render_app) = app.get_sub_app_mut(RenderApp) else { return; }; render_app.add_systems(RenderStartup, init_post_process_pipeline); render_app.add_systems( Core3d, post_process_system.in_set(Core3dSystems::PostProcess), ); } } #[derive(Default)] struct PostProcessBindGroupCache { cached: Option<(TextureViewId, BindGroup)>, } fn post_process_system( view: ViewQuery<( &ViewTarget, &PostProcessSettings, &DynamicUniformIndex, )>, post_process_pipeline: Option>, pipeline_cache: Res, settings_uniforms: Res>, mut cache: Local, mut ctx: RenderContext, ) { let Some(post_process_pipeline) = post_process_pipeline else { return; }; let (view_target, _post_process_settings, settings_index) = view.into_inner(); let Some(pipeline) = pipeline_cache.get_render_pipeline(post_process_pipeline.pipeline_id) else { return; }; let Some(settings_binding) = settings_uniforms.uniforms().binding() else { return; }; // This will start a new "post process write", obtaining two texture // views from the view target - a `source` and a `destination`. // `source` is the "current" main texture and you _must_ write into // `destination` because calling `post_process_write()` on the // [`ViewTarget`] will internally flip the [`ViewTarget`]'s main // texture to the `destination` texture. Failing to do so will cause // the current main texture information to be lost. let post_process = view_target.post_process_write(); let bind_group = match &mut cache.cached { Some((texture_id, bind_group)) if post_process.source.id() == *texture_id => bind_group, cached => { // The bind_group gets created each frame. // // Normally, you would create a bind_group in the Queue set, // but this doesn't work with the post_process_write(). // The reason it doesn't work is because each post_process_write will alternate the source/destination. // The only way to have the correct source/destination for the bind_group // is to make sure you get it during the node execution. let bind_group = ctx.render_device().create_bind_group( "post_process_bind_group", &pipeline_cache.get_bind_group_layout(&post_process_pipeline.layout), // It's important for this to match the BindGroupLayout defined in the PostProcessPipeline &BindGroupEntries::sequential(( // Make sure to use the source view post_process.source, // Use the sampler created for the pipeline &post_process_pipeline.sampler, // Set the settings binding settings_binding.clone(), )), ); let (_, bind_group) = cached.insert((post_process.source.id(), bind_group)); bind_group } }; let mut render_pass = ctx .command_encoder() .begin_render_pass(&RenderPassDescriptor { label: Some("post_process_pass"), color_attachments: &[Some(RenderPassColorAttachment { // We need to specify the post process destination view here // to make sure we write to the appropriate texture. view: post_process.destination, depth_slice: None, resolve_target: None, ops: Operations::default(), })], depth_stencil_attachment: None, timestamp_writes: None, occlusion_query_set: None, multiview_mask: None, }); render_pass.set_pipeline(pipeline); // By passing in the index of the post process settings on this view, we ensure // that in the event that multiple settings were sent to the GPU (as would be the // case with multiple cameras), we use the correct one. render_pass.set_bind_group(0, bind_group, &[settings_index.index()]); render_pass.draw(0..3, 0..1); } // This contains global data used by the render pipeline. This will be created once on startup. #[derive(Resource)] struct PostProcessPipeline { layout: BindGroupLayoutDescriptor, sampler: Sampler, pipeline_id: CachedRenderPipelineId, } fn init_post_process_pipeline( mut commands: Commands, render_device: Res, asset_server: Res, fullscreen_shader: Res, pipeline_cache: Res, ) { // We need to define the bind group layout used for our pipeline let layout = BindGroupLayoutDescriptor::new( "post_process_bind_group_layout", &BindGroupLayoutEntries::sequential( // The layout entries will only be visible in the fragment stage ShaderStages::FRAGMENT, ( // The screen texture texture_2d(TextureSampleType::Float { filterable: true }), // The sampler that will be used to sample the screen texture sampler(SamplerBindingType::Filtering), // The settings uniform that will control the effect uniform_buffer::(true), ), ), ); // We can create the sampler here since it won't change at runtime and doesn't depend on the view let sampler = render_device.create_sampler(&SamplerDescriptor::default()); // Get the shader handle let shader = asset_server.load(SHADER_ASSET_PATH); // This will setup a fullscreen triangle for the vertex state. let vertex_state = fullscreen_shader.to_vertex_state(); let pipeline_id = pipeline_cache // This will add the pipeline to the cache and queue its creation .queue_render_pipeline(RenderPipelineDescriptor { label: Some("post_process_pipeline".into()), layout: vec![layout.clone()], vertex: vertex_state, fragment: Some(FragmentState { shader, // Make sure this matches the entry point of your shader. // It can be anything as long as it matches here and in the shader. targets: vec![Some(ColorTargetState { format: TextureFormat::Rgba8UnormSrgb, blend: None, write_mask: ColorWrites::ALL, })], ..default() }), ..default() }); commands.insert_resource(PostProcessPipeline { layout, sampler, pipeline_id, }); } // This is the component that will get passed to the shader #[derive(Component, Default, Clone, Copy, ExtractComponent, ShaderType)] struct PostProcessSettings { intensity: f32, // WebGL2 structs must be 16 byte aligned. #[cfg(feature = "webgl2")] _webgl2_padding: Vec3, } /// Set up a simple 3D scene fn setup( mut commands: Commands, mut meshes: ResMut>, mut materials: ResMut>, ) { // camera // Make sure you change the TextureFormat of the ColorTargetState // if you enable Hdr directly or through features like Bloom. commands.spawn(( Camera3d::default(), Transform::from_translation(Vec3::new(0.0, 0.0, 5.0)).looking_at(Vec3::default(), Vec3::Y), Camera { clear_color: Color::WHITE.into(), ..default() }, // Add the setting to the camera. // This component is also used to determine on which camera to run the post processing effect. PostProcessSettings { intensity: 0.02, ..default() }, )); // cube commands.spawn(( Mesh3d(meshes.add(Cuboid::default())), MeshMaterial3d(materials.add(Color::srgb(0.8, 0.7, 0.6))), Transform::from_xyz(0.0, 0.5, 0.0), Rotates, )); // light commands.spawn(DirectionalLight { illuminance: 1_000., ..default() }); } #[derive(Component)] struct Rotates; /// Rotates any entity around the x and y axis fn rotate(time: Res