 fcda67e894
			
		
	
	
		fcda67e894
		
			
		
	
	
	
	
		
			
			This commit creates a new built-in postprocessing shader that's designed to hold miscellaneous postprocessing effects, and starts it off with chromatic aberration. Possible future effects include vignette, film grain, and lens distortion. [Chromatic aberration] is a common postprocessing effect that simulates lenses that fail to focus all colors of light to a single point. It's often used for impact effects and/or horror games. This patch uses the technique from *Inside* ([Gjøl & Svendsen 2016]), which allows the developer to customize the particular color pattern to achieve different effects. Unity HDRP uses the same technique, while Unreal has a hard-wired fixed color pattern. A new example, `post_processing`, has been added, in order to demonstrate the technique. The existing `post_processing` shader has been renamed to `custom_post_processing`, for clarity. [Chromatic aberration]: https://en.wikipedia.org/wiki/Chromatic_aberration [Gjøl & Svendsen 2016]: https://github.com/playdeadgames/publications/blob/master/INSIDE/rendering_inside_gdc2016.pdf   ## Changelog ### Added * Chromatic aberration is now available as a built-in postprocessing effect. To use it, add `ChromaticAberration` to your camera.
		
			
				
	
	
		
			381 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
			
		
		
	
	
			381 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
| //! 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.
 | |
| //! 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::{
 | |
|         core_3d::graph::{Core3d, Node3d},
 | |
|         fullscreen_vertex_shader::fullscreen_shader_vertex_state,
 | |
|     },
 | |
|     ecs::query::QueryItem,
 | |
|     prelude::*,
 | |
|     render::{
 | |
|         extract_component::{
 | |
|             ComponentUniforms, DynamicUniformIndex, ExtractComponent, ExtractComponentPlugin,
 | |
|             UniformComponentPlugin,
 | |
|         },
 | |
|         render_graph::{
 | |
|             NodeRunError, RenderGraphApp, RenderGraphContext, RenderLabel, ViewNode, ViewNodeRunner,
 | |
|         },
 | |
|         render_resource::{
 | |
|             binding_types::{sampler, texture_2d, uniform_buffer},
 | |
|             *,
 | |
|         },
 | |
|         renderer::{RenderContext, RenderDevice},
 | |
|         texture::BevyDefault,
 | |
|         view::ViewTarget,
 | |
|         RenderApp,
 | |
|     },
 | |
| };
 | |
| 
 | |
| /// 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 [`PostProcessingSettings`]
 | |
|             // for this plugin to work correctly.
 | |
|             ExtractComponentPlugin::<PostProcessSettings>::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::<PostProcessSettings>::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
 | |
|             // 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::<ViewNodeRunner<PostProcessNode>>(
 | |
|                 // Specify the label of the graph, in this case we want the graph for 3d
 | |
|                 Core3d,
 | |
|                 // It also needs the label of the node
 | |
|                 PostProcessLabel,
 | |
|             )
 | |
|             .add_render_graph_edges(
 | |
|                 Core3d,
 | |
|                 // Specify the node ordering.
 | |
|                 // This will automatically create all required node edges to enforce the given ordering.
 | |
|                 (
 | |
|                     Node3d::Tonemapping,
 | |
|                     PostProcessLabel,
 | |
|                     Node3d::EndMainPassPostProcessing,
 | |
|                 ),
 | |
|             );
 | |
|     }
 | |
| 
 | |
|     fn finish(&self, app: &mut App) {
 | |
|         // 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
 | |
|             // Initialize the pipeline
 | |
|             .init_resource::<PostProcessPipeline>();
 | |
|     }
 | |
| }
 | |
| 
 | |
| #[derive(Debug, Hash, PartialEq, Eq, Clone, RenderLabel)]
 | |
| struct PostProcessLabel;
 | |
| 
 | |
| // The post process node used for the render graph
 | |
| #[derive(Default)]
 | |
| struct PostProcessNode;
 | |
| 
 | |
| // 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,
 | |
|         // This makes sure the node only runs on cameras with the PostProcessSettings component
 | |
|         &'static PostProcessSettings,
 | |
|         // As there could be multiple post processing components sent to the GPU (one per camera),
 | |
|         // we need to get the index of the one that is associated with the current view.
 | |
|         &'static DynamicUniformIndex<PostProcessSettings>,
 | |
|     );
 | |
| 
 | |
|     // 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, _post_process_settings, settings_index): QueryItem<Self::ViewQuery>,
 | |
|         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::<PostProcessPipeline>();
 | |
| 
 | |
|         // 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::<PipelineCache>();
 | |
| 
 | |
|         // 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::<ComponentUniforms<PostProcessSettings>>();
 | |
|         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(
 | |
|             "post_process_bind_group",
 | |
|             &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(),
 | |
|             )),
 | |
|         );
 | |
| 
 | |
|         // 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,
 | |
|             timestamp_writes: None,
 | |
|             occlusion_query_set: 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);
 | |
|         // 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);
 | |
| 
 | |
|         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::<RenderDevice>();
 | |
| 
 | |
|         // We need to define the bind group layout used for our pipeline
 | |
|         let layout = render_device.create_bind_group_layout(
 | |
|             "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::<PostProcessSettings>(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 = world.load_asset(SHADER_ASSET_PATH);
 | |
| 
 | |
|         let pipeline_id = world
 | |
|             .resource_mut::<PipelineCache>()
 | |
|             // 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 properties are not important for this effect so just use the default values.
 | |
|                 // This struct doesn't have the Default trait 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<Assets<Mesh>>,
 | |
|     mut materials: ResMut<Assets<StandardMaterial>>,
 | |
| ) {
 | |
|     // camera
 | |
|     commands.spawn((
 | |
|         Camera3dBundle {
 | |
|             transform: Transform::from_translation(Vec3::new(0.0, 0.0, 5.0))
 | |
|                 .looking_at(Vec3::default(), Vec3::Y),
 | |
|             camera: Camera {
 | |
|                 clear_color: Color::WHITE.into(),
 | |
|                 ..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(Cuboid::default()),
 | |
|             material: materials.add(Color::srgb(0.8, 0.7, 0.6)),
 | |
|             transform: Transform::from_xyz(0.0, 0.5, 0.0),
 | |
|             ..default()
 | |
|         },
 | |
|         Rotates,
 | |
|     ));
 | |
|     // light
 | |
|     commands.spawn(DirectionalLightBundle {
 | |
|         directional_light: DirectionalLight {
 | |
|             illuminance: 1_000.,
 | |
|             ..default()
 | |
|         },
 | |
|         ..default()
 | |
|     });
 | |
| }
 | |
| 
 | |
| #[derive(Component)]
 | |
| struct Rotates;
 | |
| 
 | |
| /// Rotates any entity around the x and y axis
 | |
| fn rotate(time: Res<Time>, mut query: Query<&mut Transform, With<Rotates>>) {
 | |
|     for mut transform in &mut query {
 | |
|         transform.rotate_x(0.55 * time.delta_seconds());
 | |
|         transform.rotate_z(0.15 * time.delta_seconds());
 | |
|     }
 | |
| }
 | |
| 
 | |
| // Change the intensity over time to show that the effect is controlled from the main world
 | |
| fn update_settings(mut settings: Query<&mut PostProcessSettings>, time: Res<Time>) {
 | |
|     for mut setting in &mut settings {
 | |
|         let mut intensity = time.elapsed_seconds().sin();
 | |
|         // Make it loop periodically
 | |
|         intensity = intensity.sin();
 | |
|         // Remap it to 0..1 because the intensity can't be negative
 | |
|         intensity = intensity * 0.5 + 0.5;
 | |
|         // Scale it to a more reasonable level
 | |
|         intensity *= 0.015;
 | |
| 
 | |
|         // Set the intensity.
 | |
|         // This will then be extracted to the render world and uploaded to the gpu automatically by the [`UniformComponentPlugin`]
 | |
|         setting.intensity = intensity;
 | |
|     }
 | |
| }
 |