diff --git a/Cargo.toml b/Cargo.toml index c3d1dfb93f35f..61d0d5a580a80 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3113,6 +3113,17 @@ description = "Demonstrates how to write a specialized mesh pipeline" category = "Shaders" wasm = true +[[example]] +name = "fullscreen_material" +path = "examples/shader_advanced/fullscreen_material.rs" +doc-scrape-examples = true + +[package.metadata.example.fullscreen_material] +name = "Fullscreen Material" +description = "Demonstrates how to write a fullscreen material" +category = "Shaders Advanced" +wasm = true + # Stress tests [[package.metadata.example_category]] name = "Stress Tests" @@ -3992,6 +4003,9 @@ name = "fallback_image" path = "examples/shader/fallback_image.rs" doc-scrape-examples = true +[package.metadata.example.fallback_image] +hidden = true + [[example]] name = "reflection_probes" path = "examples/3d/reflection_probes.rs" @@ -4003,9 +4017,6 @@ description = "Demonstrates reflection probes" category = "3D Rendering" wasm = false -[package.metadata.example.fallback_image] -hidden = true - [package.metadata.example.window_resizing] name = "Window Resizing" description = "Demonstrates resizing and responding to resizing a window" diff --git a/assets/shaders/fullscreen_effect.wgsl b/assets/shaders/fullscreen_effect.wgsl new file mode 100644 index 0000000000000..b4a88cff94da2 --- /dev/null +++ b/assets/shaders/fullscreen_effect.wgsl @@ -0,0 +1,50 @@ +// This shader computes the chromatic aberration effect + +// Since post processing is a fullscreen effect, we use the fullscreen vertex shader provided by bevy. +// This will import a vertex shader that renders a single fullscreen triangle. +// +// A fullscreen triangle is a single triangle that covers the entire screen. +// The box in the top left in that diagram is the screen. The 4 x are the corner of the screen +// +// Y axis +// 1 | x-----x...... +// 0 | | s | . ´ +// -1 | x_____x´ +// -2 | : .´ +// -3 | :´ +// +--------------- X axis +// -1 0 1 2 3 +// +// As you can see, the triangle ends up bigger than the screen. +// +// You don't need to worry about this too much since bevy will compute the correct UVs for you. +#import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput + +@group(0) @binding(0) var screen_texture: texture_2d; +@group(0) @binding(1) var texture_sampler: sampler; + +struct FullScreenEffect { + intensity: f32, +#ifdef SIXTEEN_BYTE_ALIGNMENT + // WebGL2 structs must be 16 byte aligned. + _webgl2_padding: vec3 +#endif +} + +@group(0) @binding(2) var settings: FullScreenEffect; + +@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/crates/bevy_core_pipeline/src/fullscreen_material.rs b/crates/bevy_core_pipeline/src/fullscreen_material.rs new file mode 100644 index 0000000000000..4de57f54cbfc9 --- /dev/null +++ b/crates/bevy_core_pipeline/src/fullscreen_material.rs @@ -0,0 +1,329 @@ +//! This is mostly a pluginified version of the `custom_post_processing` example +//! +//! The plugin will create a new node that runs a fullscreen triangle. +//! +//! Users need to use the [`FullscreenMaterial`] trait to define the parameters like the graph label or the graph ordering. + +use core::any::type_name; +use core::marker::PhantomData; + +use crate::{core_2d::graph::Core2d, core_3d::graph::Core3d, FullscreenShader}; +use bevy_app::{App, Plugin}; +use bevy_asset::AssetServer; +use bevy_camera::{Camera2d, Camera3d}; +use bevy_ecs::{ + component::Component, + entity::Entity, + query::{Added, Has, QueryItem}, + resource::Resource, + system::{Commands, Res}, + world::{FromWorld, World}, +}; +use bevy_image::BevyDefault; +use bevy_render::{ + extract_component::{ + ComponentUniforms, DynamicUniformIndex, ExtractComponent, ExtractComponentPlugin, + UniformComponentPlugin, + }, + render_graph::{ + InternedRenderLabel, InternedRenderSubGraph, NodeRunError, RenderGraph, RenderGraphContext, + RenderGraphError, RenderGraphExt, RenderLabel, ViewNode, ViewNodeRunner, + }, + render_resource::{ + binding_types::{sampler, texture_2d, uniform_buffer}, + encase::internal::WriteInto, + BindGroupEntries, BindGroupLayoutDescriptor, BindGroupLayoutEntries, + CachedRenderPipelineId, ColorTargetState, ColorWrites, FragmentState, Operations, + PipelineCache, RenderPassColorAttachment, RenderPassDescriptor, RenderPipelineDescriptor, + Sampler, SamplerBindingType, SamplerDescriptor, ShaderStages, ShaderType, TextureFormat, + TextureSampleType, + }, + renderer::{RenderContext, RenderDevice}, + view::ViewTarget, + ExtractSchedule, MainWorld, RenderApp, RenderStartup, +}; +use bevy_shader::ShaderRef; +use bevy_utils::default; +use tracing::warn; + +#[derive(Default)] +pub struct FullscreenMaterialPlugin { + _marker: PhantomData, +} +impl Plugin for FullscreenMaterialPlugin { + fn build(&self, app: &mut App) { + app.add_plugins(( + ExtractComponentPlugin::::default(), + UniformComponentPlugin::::default(), + )); + + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app.add_systems(RenderStartup, init_pipeline::); + + if let Some(sub_graph) = T::sub_graph() { + render_app.add_render_graph_node::>>( + sub_graph, + T::node_label(), + ); + + // We can't use add_render_graph_edges because it doesn't accept a Vec + if let Some(mut render_graph) = render_app.world_mut().get_resource_mut::() + && let Some(graph) = render_graph.get_sub_graph_mut(sub_graph) + { + for window in T::node_edges().windows(2) { + let [a, b] = window else { + break; + }; + let Err(err) = graph.try_add_node_edge(*a, *b) else { + continue; + }; + match err { + // Already existing edges are very easy to produce with this api + // and shouldn't cause a panic + RenderGraphError::EdgeAlreadyExists(_) => {} + _ => panic!("{err:?}"), + } + } + } else { + warn!("Failed to add edges for FullscreenMaterial"); + }; + } else { + // If there was no sub_graph specified we try to determine the graph based on the camera + // it gets added to. + render_app.add_systems(ExtractSchedule, extract_on_add::); + } + } +} + +fn extract_on_add(world: &mut World) { + world.resource_scope::(|world, mut main_world| { + // Extract the material from the main world + let mut query = + main_world.query_filtered::<(Entity, Has, Has), Added>(); + + // Create the node and add it to the render graph + world.resource_scope::(|world, mut render_graph| { + for (_entity, is_3d, is_2d) in query.iter(&main_world) { + let graph = if is_3d && let Some(graph) = render_graph.get_sub_graph_mut(Core3d) { + graph + } else if is_2d && let Some(graph) = render_graph.get_sub_graph_mut(Core2d) { + graph + } else { + warn!("FullscreenMaterial was added to an entity that isn't a camera"); + continue; + }; + + let node = ViewNodeRunner::>::from_world(world); + graph.add_node(T::node_label(), node); + + for window in T::node_edges().windows(2) { + let [a, b] = window else { + break; + }; + let Err(err) = graph.try_add_node_edge(*a, *b) else { + continue; + }; + match err { + // Already existing edges are very easy to produce with this api + // and shouldn't cause a panic + RenderGraphError::EdgeAlreadyExists(_) => {} + _ => panic!("{err:?}"), + } + } + } + }); + }); +} + +/// A trait to define a material that will render to the entire screen using a fullscrene triangle +pub trait FullscreenMaterial: + Component + ExtractComponent + Clone + Copy + ShaderType + WriteInto + Default +{ + /// The shader that will run on the entire screen using a fullscreen triangle + fn fragment_shader() -> ShaderRef; + + /// The list of `node_edges`. In 3d, for a post processing effect, it would look like this: + /// + /// ```compile_fail + /// # use bevy_core_pipeline::core_3d::graph::Node3d; + /// # use bevy_render::render_graph::RenderLabel; + /// vec![ + /// Node3d::Tonemapping.intern(), + /// // Self::sub_graph().intern(), // <--- your own label here + /// Node3d::EndMainPassPostProcessing.intern(), + /// ] + /// ``` + /// + /// This tell the render graph to run your fullscreen effect after the tonemapping pass but + /// before the end of post processing. For 2d, it would be the same but using Node2d. You can + /// specify any edges you want but make sure to include your own label. + fn node_edges() -> Vec; + + /// The [`bevy_render::render_graph::RenderSubGraph`] the effect will run in + /// + /// For 2d this is generally [`crate::core_2d::graph::Core2d`] and for 3d it's + /// [`crate::core_3d::graph::Core3d`] + fn sub_graph() -> Option { + None + } + + /// The label used to represent the render node that will run the pass + fn node_label() -> impl RenderLabel { + FullscreenMaterialLabel(type_name::()) + } +} + +#[derive(Debug, Hash, PartialEq, Eq, Clone)] +struct FullscreenMaterialLabel(&'static str); + +impl RenderLabel for FullscreenMaterialLabel +where + Self: 'static + Send + Sync + Clone + Eq + ::core::fmt::Debug + ::core::hash::Hash, +{ + fn dyn_clone(&self) -> Box { + Box::new(::core::clone::Clone::clone(self)) + } +} + +#[derive(Resource)] +struct FullscreenMaterialPipeline { + layout: BindGroupLayoutDescriptor, + sampler: Sampler, + pipeline_id: CachedRenderPipelineId, + pipeline_id_hdr: CachedRenderPipelineId, +} + +fn init_pipeline( + mut commands: Commands, + render_device: Res, + asset_server: Res, + fullscreen_shader: Res, + pipeline_cache: Res, +) { + let layout = BindGroupLayoutDescriptor::new( + "post_process_bind_group_layout", + &BindGroupLayoutEntries::sequential( + 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), + // We use a uniform buffer so users can pass some data to the effect + // Eventually we should just use a separate bind group for user data + uniform_buffer::(true), + ), + ), + ); + let sampler = render_device.create_sampler(&SamplerDescriptor::default()); + let shader = match T::fragment_shader() { + ShaderRef::Default => { + // TODO not sure what an actual fallback should be. An empty shader or output a solid + // color to indicate a missing shader? + unimplemented!( + "FullscreenMaterial::fragment_shader() must not return ShaderRef::Default" + ) + } + ShaderRef::Handle(handle) => handle, + ShaderRef::Path(path) => asset_server.load(path), + }; + // Setup a fullscreen triangle for the vertex state. + let vertex_state = fullscreen_shader.to_vertex_state(); + let mut desc = RenderPipelineDescriptor { + label: Some("post_process_pipeline".into()), + layout: vec![layout.clone()], + vertex: vertex_state, + fragment: Some(FragmentState { + shader, + targets: vec![Some(ColorTargetState { + format: TextureFormat::bevy_default(), + blend: None, + write_mask: ColorWrites::ALL, + })], + ..default() + }), + ..default() + }; + let pipeline_id = pipeline_cache.queue_render_pipeline(desc.clone()); + desc.fragment.as_mut().unwrap().targets[0] + .as_mut() + .unwrap() + .format = ViewTarget::TEXTURE_FORMAT_HDR; + let pipeline_id_hdr = pipeline_cache.queue_render_pipeline(desc); + commands.insert_resource(FullscreenMaterialPipeline { + layout, + sampler, + pipeline_id, + pipeline_id_hdr, + }); +} + +#[derive(Default)] +struct FullscreenMaterialNode { + _marker: PhantomData, +} + +impl ViewNode for FullscreenMaterialNode { + // TODO we should expose the depth buffer and the gbuffer if using deferred + type ViewQuery = (&'static ViewTarget, &'static DynamicUniformIndex); + + fn run<'w>( + &self, + _graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + (view_target, settings_index): QueryItem, + world: &World, + ) -> Result<(), NodeRunError> { + let fullscreen_pipeline = world.resource::(); + + let pipeline_cache = world.resource::(); + let pipeline_id = if view_target.is_hdr() { + fullscreen_pipeline.pipeline_id_hdr + } else { + fullscreen_pipeline.pipeline_id + }; + + let Some(pipeline) = pipeline_cache.get_render_pipeline(pipeline_id) else { + return Ok(()); + }; + + let data_uniforms = world.resource::>(); + let Some(settings_binding) = data_uniforms.uniforms().binding() else { + return Ok(()); + }; + + let post_process = view_target.post_process_write(); + + let bind_group = render_context.render_device().create_bind_group( + "post_process_bind_group", + &pipeline_cache.get_bind_group_layout(&fullscreen_pipeline.layout), + &BindGroupEntries::sequential(( + post_process.source, + &fullscreen_pipeline.sampler, + settings_binding.clone(), + )), + ); + + let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { + label: Some("post_process_pass"), + color_attachments: &[Some(RenderPassColorAttachment { + view: post_process.destination, + depth_slice: None, + resolve_target: None, + ops: Operations::default(), + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + + render_pass.set_render_pipeline(pipeline); + render_pass.set_bind_group(0, &bind_group, &[settings_index.index()]); + render_pass.draw(0..3, 0..1); + + Ok(()) + } +} diff --git a/crates/bevy_core_pipeline/src/lib.rs b/crates/bevy_core_pipeline/src/lib.rs index 0f084152d8665..6037e89cc325d 100644 --- a/crates/bevy_core_pipeline/src/lib.rs +++ b/crates/bevy_core_pipeline/src/lib.rs @@ -11,6 +11,7 @@ pub mod core_2d; pub mod core_3d; pub mod deferred; pub mod experimental; +pub mod fullscreen_material; pub mod oit; pub mod prepass; pub mod tonemapping; diff --git a/crates/bevy_render/src/render_graph/node.rs b/crates/bevy_render/src/render_graph/node.rs index c75218c0a2941..bfc007b12754a 100644 --- a/crates/bevy_render/src/render_graph/node.rs +++ b/crates/bevy_render/src/render_graph/node.rs @@ -38,6 +38,12 @@ pub trait IntoRenderNodeArray { fn into_array(self) -> [InternedRenderLabel; N]; } +impl IntoRenderNodeArray for Vec { + fn into_array(self) -> [InternedRenderLabel; N] { + self.try_into().unwrap() + } +} + macro_rules! impl_render_label_tuples { ($N: expr, $(#[$meta:meta])* $(($T: ident, $I: ident)),*) => { $(#[$meta])* diff --git a/examples/README.md b/examples/README.md index 52f0379ea915f..06e451d4f5c91 100644 --- a/examples/README.md +++ b/examples/README.md @@ -62,6 +62,7 @@ git checkout v0.4.0 - [Scene](#scene) - [Shader Advanced](#shader-advanced) - [Shaders](#shaders) + - [Shaders Advanced](#shaders-advanced) - [State](#state) - [Stress Tests](#stress-tests) - [Time](#time) @@ -488,6 +489,12 @@ Example | Description [Storage Buffer](../examples/shader/storage_buffer.rs) | A shader that shows how to bind a storage buffer using a custom material. [Texture Binding Array (Bindless Textures)](../examples/shader_advanced/texture_binding_array.rs) | A shader that shows how to bind and sample multiple textures as a binding array (a.k.a. bindless textures). +### Shaders Advanced + +Example | Description +--- | --- +[Fullscreen Material](../examples/shader_advanced/fullscreen_material.rs) | Demonstrates how to write a fullscreen material + ### State Example | Description diff --git a/examples/shader_advanced/fullscreen_material.rs b/examples/shader_advanced/fullscreen_material.rs new file mode 100644 index 0000000000000..4cd5ed6da50e7 --- /dev/null +++ b/examples/shader_advanced/fullscreen_material.rs @@ -0,0 +1,89 @@ +//! Demonstrates how to write a custom fullscreen shader +//! +//! This is currently limited to 3d only but work is in progress to make it work in 2d + +use bevy::{ + core_pipeline::{ + core_3d::graph::Node3d, + fullscreen_material::{FullscreenMaterial, FullscreenMaterialPlugin}, + }, + prelude::*, + render::{ + extract_component::ExtractComponent, + render_graph::{InternedRenderLabel, RenderLabel}, + render_resource::ShaderType, + }, + shader::ShaderRef, +}; + +fn main() { + App::new() + .add_plugins(( + DefaultPlugins, + FullscreenMaterialPlugin::::default(), + )) + .add_systems(Startup, setup) + .run(); +} + +fn setup( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + // camera + commands.spawn(( + Camera3d::default(), + Transform::from_translation(Vec3::new(0.0, 0.0, 5.0)).looking_at(Vec3::default(), Vec3::Y), + FullscreenEffect { intensity: 0.005 }, + )); + + // cube + commands.spawn(( + Mesh3d(meshes.add(Cuboid::default())), + MeshMaterial3d(materials.add(Color::srgb(0.8, 0.7, 0.6))), + Transform::default(), + )); + + // light + commands.spawn(DirectionalLight { + illuminance: 1_000., + ..default() + }); +} + +// This is the struct that will be sent to your shader +// +// Currently, this doesn't support AsBindGroup so you can only use it to send a struct to your +// shader. We are working on adding AsBindGroup support in the future so you can bind anything you +// need. +#[derive(Component, ExtractComponent, Clone, Copy, ShaderType, Default)] +struct FullscreenEffect { + // For this example, this is used as the intensity of the effect, but you can pass in any valid + // ShaderType + // + // In the future, you will be able to use a full bind group + intensity: f32, +} + +impl FullscreenMaterial for FullscreenEffect { + // The shader that will be used + fn fragment_shader() -> ShaderRef { + "shaders/fullscreen_effect.wgsl".into() + } + + // This let's you specify a list of edges used to order when your effect pass will run + // + // This example is a post processing effect so it will run after tonemapping but before the end + // post processing pass. + // + // In 2d you would need to use [`Node2d`] instead of [`Node3d`] + fn node_edges() -> Vec { + vec![ + Node3d::Tonemapping.intern(), + // The label is automatically generated from the name of the struct + Self::node_label().intern(), + Node3d::EndMainPassPostProcessing.intern(), + ] + } +} diff --git a/release-content/release-notes/fullscreen_material.md b/release-content/release-notes/fullscreen_material.md new file mode 100644 index 0000000000000..59f0307107982 --- /dev/null +++ b/release-content/release-notes/fullscreen_material.md @@ -0,0 +1,7 @@ +--- +title: Fullscreen Material +authors: ["@IceSentry"] +pull_requests: [20414] +--- + +Users often want to run a fullscreen shader but currently the only to do this is to copy the custom_post_processing example which is very verbose and contains a lot of low level details. We introduced a new `FullscreenMaterial` trait and `FullscreenMaterialPlugin` that let you easily run a fullscreen shader and specify in which order it will run relative to other render passes in the engine.