Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
f73ccfd
WIP post_processing plugin copy
IceSentry Aug 4, 2025
79f8056
move plugin to bevy_pbr
IceSentry Aug 4, 2025
8789c3e
add some comments
IceSentry Aug 4, 2025
75c494a
customizable edges
IceSentry Aug 4, 2025
ec128ca
yeet changes to custom_post_processing
IceSentry Aug 4, 2025
6596a79
move to core_pipeline
IceSentry Aug 9, 2025
70e61bd
add custom sub_graph and more docs
IceSentry Aug 9, 2025
92f0872
fix imports
IceSentry Aug 9, 2025
9e003bd
fix example definition
IceSentry Aug 9, 2025
1e94f5e
add release notes
IceSentry Aug 9, 2025
bb1a880
fix docs
IceSentry Aug 9, 2025
8e06994
fix docs
IceSentry Aug 9, 2025
ab74519
fix docs
IceSentry Aug 9, 2025
659b77a
compile_fail
IceSentry Aug 9, 2025
de97f88
fix docs
IceSentry Aug 10, 2025
43cbadf
Merge branch 'main' into fullscreen_material
IceSentry Aug 10, 2025
44947bb
Merge branch 'main' into fullscreen_material
IceSentry Sep 5, 2025
8d37f58
Rename PostProcessSettings in shader
IceSentry Sep 10, 2025
220a9f8
Merge branch 'main' into fullscreen_material
IceSentry Nov 11, 2025
26456a1
fix after merge
IceSentry Nov 11, 2025
ce975ca
add automatic label generation
IceSentry Nov 11, 2025
bc096ec
fix ci
IceSentry Nov 11, 2025
a773d19
make sub_graph optional
IceSentry Nov 11, 2025
a613745
fix doc
IceSentry Nov 11, 2025
0b0d384
Update crates/bevy_core_pipeline/src/fullscreen_material.rs
IceSentry Dec 14, 2025
92a98e3
Update crates/bevy_core_pipeline/src/fullscreen_material.rs
IceSentry Dec 14, 2025
ae14418
Merge branch 'main' into fullscreen_material
alice-i-cecile Dec 14, 2025
1d55f78
cargo fmt
alice-i-cecile Dec 14, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3870,6 +3870,20 @@ name = "fallback_image"
path = "examples/shader/fallback_image.rs"
doc-scrape-examples = true

[package.metadata.example.fallback_image]
hidden = true

[[example]]
name = "fullscreen_material"
path = "examples/shader/fullscreen_material.rs"
doc-scrape-examples = true

[package.metadata.example.fullscreen_material]
name = "Fullscreen Material"
description = "Demonstrates how to write a fullscreen material"
category = "Shader"
wasm = true

[[example]]
name = "reflection_probes"
path = "examples/3d/reflection_probes.rs"
Expand All @@ -3881,9 +3895,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"
Expand Down
48 changes: 48 additions & 0 deletions assets/shaders/fullscreen_effect.wgsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// 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<f32>;
@group(0) @binding(1) var texture_sampler: sampler;
struct PostProcessSettings {
intensity: f32,
#ifdef SIXTEEN_BYTE_ALIGNMENT
// WebGL2 structs must be 16 byte aligned.
_webgl2_padding: vec3<f32>
#endif
}
@group(0) @binding(2) var<uniform> settings: PostProcessSettings;

@fragment
fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4<f32> {
// Chromatic aberration strength
let offset_strength = settings.intensity;

// Sample each color channel with an arbitrary shift
return vec4<f32>(
textureSample(screen_texture, texture_sampler, in.uv + vec2<f32>(offset_strength, -offset_strength)).r,
textureSample(screen_texture, texture_sampler, in.uv + vec2<f32>(-offset_strength, 0.0)).g,
textureSample(screen_texture, texture_sampler, in.uv + vec2<f32>(0.0, offset_strength)).b,
1.0
);
}


257 changes: 257 additions & 0 deletions crates/bevy_core_pipeline/src/fullscreen_material.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
//! 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::marker::PhantomData;

use crate::FullscreenShader;
use bevy_app::{App, Plugin};
use bevy_asset::AssetServer;
use bevy_ecs::{
component::Component,
query::QueryItem,
resource::Resource,
system::{Commands, Res},
world::World,
};
use bevy_image::BevyDefault;
use bevy_render::{
extract_component::{
ComponentUniforms, DynamicUniformIndex, ExtractComponent, ExtractComponentPlugin,
UniformComponentPlugin,
},
render_graph::{
InternedRenderLabel, NodeRunError, RenderGraph, RenderGraphContext, RenderGraphError,
RenderGraphExt, RenderLabel, RenderSubGraph, ViewNode, ViewNodeRunner,
},
render_resource::{
binding_types::{sampler, texture_2d, uniform_buffer},
encase::internal::WriteInto,
BindGroupEntries, BindGroupLayout, BindGroupLayoutEntries, CachedRenderPipelineId,
ColorTargetState, ColorWrites, FragmentState, Operations, PipelineCache,
RenderPassColorAttachment, RenderPassDescriptor, RenderPipelineDescriptor, Sampler,
SamplerBindingType, SamplerDescriptor, ShaderRef, ShaderStages, ShaderType, TextureFormat,
TextureSampleType,
},
renderer::{RenderContext, RenderDevice},
view::ViewTarget,
RenderApp, RenderStartup,
};
use bevy_utils::default;
use tracing::warn;

#[derive(Default)]
pub struct FullscreenMaterialPlugin<T: FullscreenMaterial> {
_marker: PhantomData<T>,
}
impl<T: FullscreenMaterial> Plugin for FullscreenMaterialPlugin<T> {
fn build(&self, app: &mut App) {
app.add_plugins((
ExtractComponentPlugin::<T>::default(),
UniformComponentPlugin::<T>::default(),
));

let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app.add_systems(RenderStartup, init_pipeline::<T>);

render_app.add_render_graph_node::<ViewNodeRunner<FullscreenMaterialNode<T>>>(
T::sub_graph(),
T::node_label(),
);
// We can't use add_render_graph_edges because it doesn't accept a Vec<RenderLabel>
if let Some(mut render_graph) = render_app.world_mut().get_resource_mut::<RenderGraph>()
&& let Some(graph) = render_graph.get_sub_graph_mut(T::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");
};
}
}

/// 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 [`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() -> impl RenderSubGraph;
/// The label used to represent the render node that will run the pass
fn node_label() -> impl RenderLabel;
/// 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment is wrong.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's correct? It's just explaining the code snippet just above it.

/// 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<InternedRenderLabel>;
}

#[derive(Resource)]
struct FullscreenMaterialPipeline {
layout: BindGroupLayout,
sampler: Sampler,
pipeline_id: CachedRenderPipelineId,
pipeline_id_hdr: CachedRenderPipelineId,
}

fn init_pipeline<T: FullscreenMaterial>(
mut commands: Commands,
render_device: Res<RenderDevice>,
asset_server: Res<AssetServer>,
fullscreen_shader: Res<FullscreenShader>,
pipeline_cache: Res<PipelineCache>,
) {
let layout = render_device.create_bind_group_layout(
"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::<T>(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!("No default fallback for FullscreenMaterial shader")
}
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<T: FullscreenMaterial> {
_marker: PhantomData<T>,
}

impl<T: FullscreenMaterial> ViewNode for FullscreenMaterialNode<T> {
// TODO we should expose the depth buffer and the gbuffer if using deferred
type ViewQuery = (&'static ViewTarget, &'static DynamicUniformIndex<T>);

fn run<'w>(
&self,
_graph: &mut RenderGraphContext,
render_context: &mut RenderContext,
(view_target, settings_index): QueryItem<Self::ViewQuery>,
world: &World,
) -> Result<(), NodeRunError> {
let fullscreen_pipeline = world.resource::<FullscreenMaterialPipeline>();

let pipeline_cache = world.resource::<PipelineCache>();
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::<ComponentUniforms<T>>();
let Some(settings_binding) = data_uniforms.uniforms().binding() else {
return Ok(());
};

// We should maybe rename this because this can be used for other reasons that aren't
// post-processing
let post_process = view_target.post_process_write();

let bind_group = render_context.render_device().create_bind_group(
"post_process_bind_group",
&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(())
}
}
1 change: 1 addition & 0 deletions crates/bevy_core_pipeline/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub mod core_3d;
pub mod deferred;
pub mod dof;
pub mod experimental;
pub mod fullscreen_material;
pub mod motion_blur;
pub mod msaa_writeback;
pub mod oit;
Expand Down
6 changes: 6 additions & 0 deletions crates/bevy_render/src/render_graph/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ pub trait IntoRenderNodeArray<const N: usize> {
fn into_array(self) -> [InternedRenderLabel; N];
}

impl<const N: usize> IntoRenderNodeArray<N> for Vec<InternedRenderLabel> {
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])*
Expand Down
7 changes: 7 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ git checkout v0.4.0
- [Reflection](#reflection)
- [Remote Protocol](#remote-protocol)
- [Scene](#scene)
- [Shader](#shader)
- [Shaders](#shaders)
- [State](#state)
- [Stress Tests](#stress-tests)
Expand Down Expand Up @@ -446,6 +447,12 @@ Example | Description
--- | ---
[Scene](../examples/scene/scene.rs) | Demonstrates loading from and saving scenes to files

### Shader

Example | Description
--- | ---
[Fullscreen Material](../examples/shader/fullscreen_material.rs) | Demonstrates how to write a fullscreen material

### Shaders

These examples demonstrate how to implement different shaders in user code.
Expand Down
Loading
Loading