From 6c2518b71d6e9977e83134f15234f8b8f00b0923 Mon Sep 17 00:00:00 2001 From: IceSentry Date: Wed, 15 Mar 2023 01:04:09 -0400 Subject: [PATCH 01/21] Basic example --- Cargo.toml | 12 +- assets/shaders/post_process_pass.wgsl | 32 ++ examples/README.md | 4 +- examples/shader/post_process_pass.rs | 402 ++++++++++++++++++++++++++ 4 files changed, 448 insertions(+), 2 deletions(-) create mode 100644 assets/shaders/post_process_pass.wgsl create mode 100644 examples/shader/post_process_pass.rs diff --git a/Cargo.toml b/Cargo.toml index 47d8ea3c9fe93..d47dab1d6f7af 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1385,11 +1385,21 @@ name = "post_processing" path = "examples/shader/post_processing.rs" [package.metadata.example.post_processing] -name = "Post Processing" +name = "Post Processing - Render To Texture" description = "A custom post processing effect, using two cameras, with one reusing the render texture of the first one" category = "Shaders" wasm = true +[[example]] +name = "post_process_pass" +path = "examples/shader/post_process_pass.rs" + +[package.metadata.example.post_process_pass] +name = "Post Processing - Custom Render Pass" +description = "A custom post processing effect, using a custom render pass that runs after the main pass" +category = "Shaders" +wasm = true + [[example]] name = "shader_defs" path = "examples/shader/shader_defs.rs" diff --git a/assets/shaders/post_process_pass.wgsl b/assets/shaders/post_process_pass.wgsl new file mode 100644 index 0000000000000..d37a740026ac7 --- /dev/null +++ b/assets/shaders/post_process_pass.wgsl @@ -0,0 +1,32 @@ +// This shader computes the chromatic aberration effect + +#import bevy_pbr::utils + +// Since post process is a fullscreen effect, we use the fullscreen vertex stage from bevy +// This will render a single fullscreen triangle. +#import bevy_core_pipeline::fullscreen_vertex_shader + +@group(0) @binding(0) +var screen_texture: texture_2d; +@group(0) @binding(1) +var texture_sampler: sampler; +struct PostProcessSettings { + intensity: f32, +} +@group(0) @binding(2) +var settings: PostProcessSettings; + +@fragment +fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4 { + // Chromatic aberration strength + let offset_strength = settings.intensity; + + // Sample each color channel with an arbitrary shift + return vec4( + textureSample(screen_texture, texture_sampler, in.uv + vec2(offset_strength, -offset_strength)).r, + textureSample(screen_texture, texture_sampler, in.uv + vec2(-offset_strength, 0.0)).g, + textureSample(screen_texture, texture_sampler, in.uv + vec2(0.0, offset_strength)).b, + 1.0 + ); +} + diff --git a/examples/README.md b/examples/README.md index 8fd8f7c29b8d0..543ef189229ef 100644 --- a/examples/README.md +++ b/examples/README.md @@ -277,7 +277,9 @@ Example | Description [Material](../examples/shader/shader_material.rs) | A shader and a material that uses it [Material - GLSL](../examples/shader/shader_material_glsl.rs) | A shader that uses the GLSL shading language [Material - Screenspace Texture](../examples/shader/shader_material_screenspace_texture.rs) | A shader that samples a texture with view-independent UV coordinates -[Material Prepass](../examples/shader/shader_prepass.rs) | A shader that uses the various textures generated by the prepass +[Material Prepass](../examples/shader/shader_prepass.rs) | A shader that uses the depth texture generated in a prepass +[Post Processing - Custom Render Pass](../examples/shader/post_process_pass.rs) | A custom post processing effect, using a custom render pass that runs after the main pass +[Post Processing - Render To Texture](../examples/shader/post_processing.rs) | A custom post processing effect, using two cameras, with one reusing the render texture of the first one [Post Processing](../examples/shader/post_processing.rs) | A custom post processing effect, using two cameras, with one reusing the render texture of the first one [Shader Defs](../examples/shader/shader_defs.rs) | A shader that uses "shaders defs" (a bevy tool to selectively toggle parts of a shader) [Texture Binding Array (Bindless Textures)](../examples/shader/texture_binding_array.rs) | A shader that shows how to bind and sample multiple textures as a binding array (a.k.a. bindless textures). diff --git a/examples/shader/post_process_pass.rs b/examples/shader/post_process_pass.rs new file mode 100644 index 0000000000000..32d49e2aac29d --- /dev/null +++ b/examples/shader/post_process_pass.rs @@ -0,0 +1,402 @@ +//! 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::{ + core_pipeline::{ + clear_color::ClearColorConfig, core_3d, + fullscreen_vertex_shader::fullscreen_shader_vertex_state, + }, + prelude::*, + render::{ + extract_component::{ + ComponentUniforms, ExtractComponent, ExtractComponentPlugin, UniformComponentPlugin, + }, + render_graph::{Node, NodeRunError, RenderGraph, RenderGraphContext, SlotInfo, SlotType}, + 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::{ExtractedView, ViewTarget}, + Extract, RenderApp, + }, +}; + +fn main() { + App::new() + .add_plugins(DefaultPlugins.set(AssetPlugin { + // Hot reloading the shader works correctly + watch_for_changes: true, + ..default() + })) + .add_plugin(PostProcessPlugin) + .add_startup_system(setup) + .add_system(rotate) + .add_system(update_settings) + .run(); +} + +/// It's generally encouraged to setup post processing effect as a plugin +struct PostProcessPlugin; +impl Plugin for PostProcessPlugin { + fn build(&self, app: &mut App) { + app + // 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. + .add_plugin(ExtractComponentPlugin::::default()) + // The settings will use a uniform buffer, so we need this plugin to let bevy manage this automatically. + .add_plugin(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; + }; + + // The renderer has multiple stages. + // For more details of each stage see the docs for `RenderSet` + render_app + .init_resource::() + .add_system(extract_post_process_settings.in_schedule(ExtractSchedule)); + + // Create our node with the render world + let node = PostProcessNode::new(&mut render_app.world); + + // Get the render graph for the entire app + let mut graph = render_app.world.resource_mut::(); + + // Get the render graph for 3d entities + let core_3d_graph = graph.get_sub_graph_mut(core_3d::graph::NAME).unwrap(); + + // Register the post process node in the 3d render graph + core_3d_graph.add_node(PostProcessNode::NAME, node); + + // A slot edge tells the render graph which input/output value should be passed to the node. + core_3d_graph.add_slot_edge( + core_3d_graph.input_node().id, + core_3d::graph::input::VIEW_ENTITY, + PostProcessNode::NAME, + PostProcessNode::IN_VIEW, + ); + + // We now need to add an edge between our node and the nodes from bevy + // to make sure our node is scheduled correctly. + // + // Here we want our effect to run after tonemapping and before the end of the main pass + core_3d_graph.add_node_edge(core_3d::graph::node::TONEMAPPING, PostProcessNode::NAME); + core_3d_graph.add_node_edge( + PostProcessNode::NAME, + core_3d::graph::node::END_MAIN_PASS_POST_PROCESSING, + ); + } +} + +/// The post process node used for the render graph +struct PostProcessNode { + // The node needs a query to know how to render, + // but it's not a normal system so we need to define it manually. + query: QueryState<&'static ViewTarget, With>, +} + +impl PostProcessNode { + pub const IN_VIEW: &str = "view"; + pub const NAME: &str = "post_process"; + + fn new(world: &mut World) -> Self { + Self { + query: QueryState::new(world), + } + } +} + +impl Node for PostProcessNode { + // This defines the input slot of the node and tells the render graph what + // we will need when running the node. + fn input(&self) -> Vec { + // In this case we tell the graph that our node will use the view entity. + // Currently, every node in bevy uses this pattern, so it's safe to just copy it. + vec![SlotInfo::new(PostProcessNode::IN_VIEW, SlotType::Entity)] + } + + // This will run every frame before the run() method + // The important difference is that `self` is `mut` here + fn update(&mut self, world: &mut World) { + // Since this is not a system we need to update the query manually. + self.query.update_archetypes(world); + } + + // Runs the node logic + // This is where you issue draw calls. + fn run( + &self, + graph_context: &mut RenderGraphContext, + render_context: &mut RenderContext, + world: &World, + ) -> Result<(), NodeRunError> { + // Get the entity of the view for the render graph where this node is running + let view_entity = graph_context.get_input_entity(PostProcessNode::IN_VIEW)?; + + // We get the data we need from the world based on the view entity. + let Ok(view_target) = self.query.get_manual(world, view_entity) else { + return Ok(()); + }; + + // 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's required to avoid creating a new pipeline each frame. + let pipeline_cache = world.resource::(); + + // Get the pipeline data for the current view + 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(()); + }; + + // Get the TextureView used for post processing effects in bevy + let post_process = view_target.post_process_write(); + + // The bind_group gets created each frame. + // It's important for this to match the BindGroupLayout defined in the PostProcessPipeline + let bind_group = render_context + .render_device() + .create_bind_group(&BindGroupDescriptor { + label: Some("post_process_bind_group"), + layout: &post_process_pipeline.layout, + 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, + }); + + // Set the render pipeline for this render pass + render_pass.set_render_pipeline(pipeline); + + // Set the bind group + render_pass.set_bind_group(0, &bind_group, &[]); + + // In this case we want to draw a fullscreen triangle, so we just need to send a single draw call. + 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: None, + }, + 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_process_pass.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 +// Since it's going to be a uniform. Don't forget to use the UniformComponentPlugin +#[derive(Component, Default, Clone, Copy, ExtractComponent, ShaderType)] +struct PostProcessSettings { + intensity: f32, +} + +// The extract stage is the only sync point between the main world and the render world. +// This is where you can get data from the main app to the render app. Like in this case for the settings of the effect. +// +// It's recommended to keep this stage as simple as possible because it blocks the render thread. +fn extract_post_process_settings( + mut commands: Commands, + cameras_2d: Extract>>, +) { + for (entity, camera, settings) in &cameras_2d { + if camera.is_active { + // Add the PostProcessSettings component to the camera so it can be queried in the render node. + commands.get_or_spawn(entity).insert(*settings); + } + } +} + +/// Set up a simple 3D scene +fn setup( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + // 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() + }); + // 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 }, + )); +} + +#[derive(Component)] +struct Rotates; + +/// Rotates any entity around the x and y axis +fn rotate(time: Res