//! This example shows how to create a custom render pass 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. //! //! This is a fairly low level example and assumes some familiarity with rendering concepts and wgpu. use bevy::{ asset::ChangeWatcher, core_pipeline::{ clear_color::ClearColorConfig, core_3d, fullscreen_vertex_shader::fullscreen_shader_vertex_state, }, prelude::*, render::{ extract_component::{ ComponentUniforms, ExtractComponent, ExtractComponentPlugin, UniformComponentPlugin, }, render_graph::{NodeRunError, RenderGraphApp, RenderGraphContext}, render_resource::{ BindGroupDescriptor, BindGroupEntry, BindGroupLayout, BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingResource, BindingType, CachedRenderPipelineId, ColorTargetState, ColorWrites, FragmentState, MultisampleState, Operations, PipelineCache, PrimitiveState, RenderPassColorAttachment, RenderPassDescriptor, RenderPipelineDescriptor, Sampler, SamplerBindingType, SamplerDescriptor, ShaderStages, ShaderType, TextureFormat, TextureSampleType, TextureViewDimension, }, renderer::{RenderContext, RenderDevice}, texture::BevyDefault, view::ViewTarget, RenderApp, }, utils::Duration, }; use bevy_internal::{ ecs::query::QueryItem, render::render_graph::{ViewNode, ViewNodeRunner}, }; fn main() { App::new() .add_plugins(( DefaultPlugins.set(AssetPlugin { // Hot reloading the shader works correctly watch_for_changes: ChangeWatcher::with_delay(Duration::from_millis(200)), ..default() }), 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 [`PostProcessingSettings`] // 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 Ok(render_app) = app.get_sub_app_mut(RenderApp) else { return; }; render_app // Bevy's renderer uses a render graph which is a collection of nodes in a directed acyclic graph. // It currently runs on each view/camera and executes each node in the specified order. // It will make sure that any node that needs a dependency from another node // only runs when that dependency is done. // // Each node can execute arbitrary work, but it generally runs at least one render pass. // A node only has access to the render world, so if you need data from the main world // you need to extract it manually or with the plugin like above. // Add a [`Node`] to the [`RenderGraph`] // The Node needs to impl FromWorld // // The [`ViewNodeRunner`] is a special [`Node`] that will automatically run the node for each view // matching the [`ViewQuery`] .add_render_graph_node::>( // Specify the name of the graph, in this case we want the graph for 3d core_3d::graph::NAME, // It also needs the name of the node PostProcessNode::NAME, ) .add_render_graph_edges( core_3d::graph::NAME, // Specify the node ordering. // This will automatically create all required node edges to enforce the given ordering. &[ core_3d::graph::node::TONEMAPPING, PostProcessNode::NAME, core_3d::graph::node::END_MAIN_PASS_POST_PROCESSING, ], ); } fn finish(&self, app: &mut App) { // We need to get the render app from the main app let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { return; }; render_app // Initialize the pipeline .init_resource::(); } } // The post process node used for the render graph #[derive(Default)] struct PostProcessNode; impl PostProcessNode { pub const NAME: &str = "post_process"; } // The ViewNode trait is required by the ViewNodeRunner impl ViewNode for PostProcessNode { // The node needs a query to gather data from the ECS in order to do its rendering, // but it's not a normal system so we need to define it manually. // // This query will only run on the view entity type ViewQuery = &'static ViewTarget; // Runs the node logic // This is where you encode draw commands. // // This will run on every view on which the graph is running. // If you don't want your effect to run on every camera, // you'll need to make sure you have a marker component as part of [`ViewQuery`] // to identify which camera(s) should run the effect. fn run( &self, _graph: &mut RenderGraphContext, render_context: &mut RenderContext, view_target: QueryItem, world: &World, ) -> Result<(), NodeRunError> { // Get the pipeline resource that contains the global data we need // to create the render pipeline let post_process_pipeline = world.resource::(); // The pipeline cache is a cache of all previously created pipelines. // It is required to avoid creating a new pipeline each frame, // which is expensive due to shader compilation. let pipeline_cache = world.resource::(); // Get the pipeline from the cache let Some(pipeline) = pipeline_cache.get_render_pipeline(post_process_pipeline.pipeline_id) else { return Ok(()); }; // Get the settings uniform binding let settings_uniforms = world.resource::>(); let Some(settings_binding) = settings_uniforms.uniforms().binding() else { return Ok(()); }; // 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(); // 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 = render_context .render_device() .create_bind_group(&BindGroupDescriptor { label: Some("post_process_bind_group"), layout: &post_process_pipeline.layout, // It's important for this to match the BindGroupLayout defined in the PostProcessPipeline entries: &[ BindGroupEntry { binding: 0, // Make sure to use the source view resource: BindingResource::TextureView(post_process.source), }, BindGroupEntry { binding: 1, // Use the sampler created for the pipeline resource: BindingResource::Sampler(&post_process_pipeline.sampler), }, BindGroupEntry { binding: 2, // Set the settings binding resource: settings_binding.clone(), }, ], }); // Begin the render pass let mut render_pass = render_context.begin_tracked_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, resolve_target: None, ops: Operations::default(), })], depth_stencil_attachment: None, }); // This is mostly just wgpu boilerplate for drawing a fullscreen triangle, // using the pipeline/bind_group created above render_pass.set_render_pipeline(pipeline); render_pass.set_bind_group(0, &bind_group, &[]); render_pass.draw(0..3, 0..1); Ok(()) } } // This contains global data used by the render pipeline. This will be created once on startup. #[derive(Resource)] struct PostProcessPipeline { layout: BindGroupLayout, sampler: Sampler, pipeline_id: CachedRenderPipelineId, } impl FromWorld for PostProcessPipeline { fn from_world(world: &mut World) -> Self { let render_device = world.resource::(); // We need to define the bind group layout used for our pipeline let layout = render_device.create_bind_group_layout(&BindGroupLayoutDescriptor { label: Some("post_process_bind_group_layout"), entries: &[ // The screen texture BindGroupLayoutEntry { binding: 0, visibility: ShaderStages::FRAGMENT, ty: BindingType::Texture { sample_type: TextureSampleType::Float { filterable: true }, view_dimension: TextureViewDimension::D2, multisampled: false, }, count: None, }, // The sampler that will be used to sample the screen texture BindGroupLayoutEntry { binding: 1, visibility: ShaderStages::FRAGMENT, ty: BindingType::Sampler(SamplerBindingType::Filtering), count: None, }, // The settings uniform that will control the effect BindGroupLayoutEntry { binding: 2, visibility: ShaderStages::FRAGMENT, ty: BindingType::Buffer { ty: bevy::render::render_resource::BufferBindingType::Uniform, has_dynamic_offset: false, min_binding_size: Some(PostProcessSettings::min_size()), }, count: None, }, ], }); // 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 = world .resource::() .load("shaders/post_processing.wgsl"); let pipeline_id = world .resource_mut::() // This will add the pipeline to the cache and queue it's creation .queue_render_pipeline(RenderPipelineDescriptor { label: Some("post_process_pipeline".into()), layout: vec![layout.clone()], // This will setup a fullscreen triangle for the vertex state vertex: fullscreen_shader_vertex_state(), fragment: Some(FragmentState { shader, shader_defs: vec![], // Make sure this matches the entry point of your shader. // It can be anything as long as it matches here and in the shader. entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { format: TextureFormat::bevy_default(), blend: None, write_mask: ColorWrites::ALL, })], }), // All of the following property are not important for this effect so just use the default values. // This struct doesn't have the Default trai implemented because not all field can have a default value. primitive: PrimitiveState::default(), depth_stencil: None, multisample: MultisampleState::default(), push_constant_ranges: vec![], }); Self { 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 commands.spawn(( Camera3dBundle { transform: Transform::from_translation(Vec3::new(0.0, 0.0, 5.0)) .looking_at(Vec3::default(), Vec3::Y), camera_3d: Camera3d { clear_color: ClearColorConfig::Custom(Color::WHITE), ..default() }, ..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(( PbrBundle { mesh: meshes.add(Mesh::from(shape::Cube { size: 1.0 })), material: materials.add(Color::rgb(0.8, 0.7, 0.6).into()), transform: Transform::from_xyz(0.0, 0.5, 0.0), ..default() }, Rotates, )); // light commands.spawn(PointLightBundle { transform: Transform::from_translation(Vec3::new(0.0, 0.0, 10.0)), ..default() }); } #[derive(Component)] struct Rotates; /// Rotates any entity around the x and y axis fn rotate(time: Res