//! 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, Render, RenderApp, RenderStartup, RenderSystems, }, }; /// This example uses a shader source file from the assets subdirectory const SHADER_ASSET_PATH: &str = "shaders/post_processing.wesl"; 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) .add_systems( Render, prepare_bind_groups.in_set(RenderSystems::PrepareBindGroups), ) .add_systems( Core3d, post_process_system.in_set(Core3dSystems::PostProcess), ); } } /// Holds the bind groups for both main textures /// /// We can't know ahead of time which one is the source or destination so we create a bind group /// for both #[derive(Component)] struct PostProcessBindGroups { a: (TextureViewId, BindGroup), b: (TextureViewId, BindGroup), } /// Create the bind groups for both main textures /// /// We will pick the correct one in the encoding system fn prepare_bind_groups( mut commands: Commands, mut views: Query<(Entity, &ViewTarget, Option<&mut PostProcessBindGroups>)>, post_process_pipeline: Option>, pipeline_cache: Res, settings_uniforms: Res>, render_device: Res, ) { let Some(post_process_pipeline) = post_process_pipeline else { return; }; let Some(settings_binding) = settings_uniforms.uniforms().binding() else { return; }; let create_bind_group = |texture: &TextureView| { ( texture.id(), render_device.create_bind_group( "post_process_bind_group", &pipeline_cache.get_bind_group_layout(&post_process_pipeline.layout), &BindGroupEntries::sequential(( texture, &post_process_pipeline.sampler, settings_binding.clone(), )), ), ) }; for (entity, view_target, mut maybe_bind_groups) in &mut views { let main_texture_view = view_target.main_texture_view(); let main_texture_other_view = view_target.main_texture_other_view(); // Only update the cached bind groups if the main texture has changed if let Some(bind_groups) = &mut maybe_bind_groups { if bind_groups.a.0 != main_texture_view.id() { bind_groups.a = create_bind_group(main_texture_view); } if bind_groups.b.0 != main_texture_other_view.id() { bind_groups.b = create_bind_group(main_texture_other_view); } } else { // Create the bind groups and add them to the view commands.entity(entity).insert(PostProcessBindGroups { a: create_bind_group(main_texture_view), b: create_bind_group(main_texture_other_view), }); } } } fn post_process_system( view: ViewQuery<( &ViewTarget, &DynamicUniformIndex, &PostProcessBindGroups, )>, post_process_pipeline: Option>, pipeline_cache: Res, mut ctx: RenderContext, ) { let Some(post_process_pipeline) = post_process_pipeline else { return; }; let (view_target, settings_index, bind_groups) = view.into_inner(); let Some(pipeline) = pipeline_cache.get_render_pipeline(post_process_pipeline.pipeline_id) 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(); // We prepared the bind group ahead of time but now we need to make sure // we pick the bind group associated with the source texture let (_, bind_group) = if bind_groups.a.0 == post_process.source.id() { &bind_groups.a } else { &bind_groups.b }; 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)] #[extract_app(RenderApp)] 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