Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Merged by Bors] - add a post-processing example #4797

Closed
wants to merge 17 commits into from
Closed
Show file tree
Hide file tree
Changes from 10 commits
Commits
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
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,10 @@ path = "examples/scene/scene.rs"
name = "custom_vertex_attribute"
path = "examples/shader/custom_vertex_attribute.rs"

[[example]]
name = "post_processing"
path = "examples/shader/post_processing.rs"

[[example]]
name = "shader_defs"
path = "examples/shader/shader_defs.rs"
Expand Down
25 changes: 25 additions & 0 deletions assets/shaders/custom_material_chromatic_aberration.wgsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#import bevy_pbr::mesh_view_bindings

[[group(1), binding(0)]]
var texture: texture_2d<f32>;

[[group(1), binding(1)]]
var our_sampler: sampler;


[[stage(fragment)]]
fn fragment([[builtin(position)]] position: vec4<f32>) -> [[location(0)]] vec4<f32> {
// Get screen position with coordinates from 0 to 1
let uv = position.xy / vec2<f32>(view.width, view.height);
let offset_strength = 0.02;

// Sample each color channel with an arbitrary shift
var output_color = vec4<f32>(
textureSample(texture, our_sampler, uv + vec2<f32>(offset_strength, -offset_strength)).r,
textureSample(texture, our_sampler, uv + vec2<f32>(-offset_strength, 0.0)).g,
textureSample(texture, our_sampler, uv + vec2<f32>(0.0, offset_strength)).b,
1.0
);

return output_color;
}
Vrixyz marked this conversation as resolved.
Show resolved Hide resolved
Vrixyz marked this conversation as resolved.
Show resolved Hide resolved
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ Example | File | Description
`animate_shader` | [`shader/animate_shader.rs`](./shader/animate_shader.rs) | A shader that uses dynamic data like the time since startup.
`compute_shader_game_of_life` | [`shader/compute_shader_game_of_life.rs`](./shader/compute_shader_game_of_life.rs) | A compute shader that simulates Conway's Game of Life.
`custom_vertex_attribute` | [`shader/custom_vertex_attribute.rs`](./shader/custom_vertex_attribute.rs) | A shader that reads a mesh's custom vertex attribute.
`post_processing` | [`shader/post_processing.rs`](./shader/post_processing.rs) | A custom post processing effect, using two render passes and a custom shader.
`shader_defs` | [`shader/shader_defs.rs`](./shader/shader_defs.rs) | A shader that uses "shaders defs" (a bevy tool to selectively toggle parts of a shader).
`shader_instancing` | [`shader/shader_instancing.rs`](./shader/shader_instancing.rs) | A shader that renders a mesh multiple times in one draw call.
`shader_material` | [`shader/shader_material.rs`](./shader/shader_material.rs) | A shader and a material that uses it.
Expand Down
265 changes: 265 additions & 0 deletions examples/shader/post_processing.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
//! A custom post processing effect, using two render passes and a custom shader.
//! Here a chromatic aberration is applied to a 3d scene containting a rotating cube.
//! This example is useful to implement your own post-processing effect such as
//! edge detection, blur, pixelization, vignette... and countless others.

use bevy::{
core_pipeline::clear_color::ClearColorConfig,
ecs::system::{lifetimeless::SRes, SystemParamItem},
prelude::*,
reflect::TypeUuid,
render::{
camera::{Camera, RenderTarget},
render_asset::{PrepareAssetError, RenderAsset, RenderAssets},
render_graph::{Node, NodeRunError, RenderGraph, RenderGraphContext, SlotValue},
render_resource::{
BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout,
BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingResource, BindingType,
Extent3d, SamplerBindingType, ShaderStages, TextureDescriptor, TextureDimension,
TextureFormat, TextureSampleType, TextureUsages, TextureViewDimension,
},
renderer::{RenderContext, RenderDevice},
view::RenderLayers,
RenderApp,
},
sprite::{Material2d, Material2dPipeline, Material2dPlugin, MaterialMesh2dBundle},
};

#[derive(Component, Default)]
pub struct PostProcessingPassCamera;
Vrixyz marked this conversation as resolved.
Show resolved Hide resolved

/// The name of the final node of the post process pass.
pub const POST_PROCESS_PASS_DRIVER: &str = "post_process_pass_driver";
Vrixyz marked this conversation as resolved.
Show resolved Hide resolved

fn main() {
let mut app = App::new();
app.add_plugins(DefaultPlugins)
.add_plugin(Material2dPlugin::<PostProcessingMaterial>::default())
.add_startup_system(setup)
.add_system(main_pass_cube_rotator_system);

app.run();
}

/// Marks the Main pass cube (rendered to a texture.)
#[derive(Component)]
struct MainPassCube;

fn setup(
mut commands: Commands,
mut windows: ResMut<Windows>,
mut meshes: ResMut<Assets<Mesh>>,
mut post_processing_materials: ResMut<Assets<PostProcessingMaterial>>,
mut materials: ResMut<Assets<StandardMaterial>>,
mut images: ResMut<Assets<Image>>,
) {
let window = windows.get_primary_mut().unwrap();
let size = Extent3d {
width: window.physical_width(),
height: window.physical_height(),
..default()
};

// This is the texture that will be rendered to.
let mut image = Image {
texture_descriptor: TextureDescriptor {
label: None,
size,
dimension: TextureDimension::D2,
format: TextureFormat::Bgra8UnormSrgb,
mip_level_count: 1,
sample_count: 1,
usage: TextureUsages::TEXTURE_BINDING
| TextureUsages::COPY_DST
| TextureUsages::RENDER_ATTACHMENT,
},
..default()
};

// fill image.data with zeroes
image.resize(size);

let image_handle = images.add(image);
Vrixyz marked this conversation as resolved.
Show resolved Hide resolved

let cube_handle = meshes.add(Mesh::from(shape::Cube { size: 4.0 }));
let cube_material_handle = materials.add(StandardMaterial {
base_color: Color::rgb(0.8, 0.7, 0.6),
reflectance: 0.02,
unlit: false,
..default()
});

// The cube that will be rendered to the texture.
commands
.spawn_bundle(PbrBundle {
mesh: cube_handle,
material: cube_material_handle,
transform: Transform::from_translation(Vec3::new(0.0, 0.0, 1.0)),
..default()
})
.insert(MainPassCube);

// Light
// NOTE: Currently lights are shared between passes - see https://github.com/bevyengine/bevy/issues/3462
commands.spawn_bundle(PointLightBundle {
transform: Transform::from_translation(Vec3::new(0.0, 0.0, 10.0)),
..default()
});

// Main pass camera
commands.spawn_bundle(Camera3dBundle {
camera_3d: Camera3d {
clear_color: ClearColorConfig::Custom(Color::WHITE),
},
camera: Camera {
target: RenderTarget::Image(image_handle.clone()),
..default()
},
transform: Transform::from_translation(Vec3::new(0.0, 0.0, 15.0))
.looking_at(Vec3::default(), Vec3::Y),
..default()
});

// This specifies the layer used for the post processing pass, which will be attached to the post processing pass camera and 2d quad.
let post_processing_pass_layer = RenderLayers::layer((RenderLayers::TOTAL_LAYERS - 1) as u8);
Vrixyz marked this conversation as resolved.
Show resolved Hide resolved

let quad_handle = meshes.add(Mesh::from(shape::Quad::new(Vec2::new(
size.width as f32,
size.height as f32,
))));

// This material has the texture that has been rendered.
let material_handle = post_processing_materials.add(PostProcessingMaterial {
source_image: image_handle,
});

// Post processing pass 2d quad, with material containing the rendered main pass texture, with a custom shader.
commands
.spawn_bundle(MaterialMesh2dBundle {
mesh: quad_handle.into(),
Copy link
Contributor

Choose a reason for hiding this comment

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

Using a quad is fine but also suboptimal. A fullscreen triangle generated from vertex indices would be better, though drawing something which has no mesh entity is a bit more tricky I would think.

Copy link
Member Author

Choose a reason for hiding this comment

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

I thought fullscreen triangle meant a triangle mesh covering all screen (so having corners outside of the screen). I guess I could modify the quad mesh into a triangle, compute its correct size and adapt uv values (or shader).

Your comment about “no mesh entity” makes me wonder if you meant something different ? A geometry shader ? Oh a compute shader applied to the render without needing a cpu mesh..? Sounds less trivial indeed

Copy link
Contributor

Choose a reason for hiding this comment

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

By fullscreen triangle, I mean a triangle with one corner in one of the corners of the screen, one at the same y and twice the width, and one at the same x and twice the height. The uvs at those vertices are similarly 0,0, 2,0, and 0,2. This makes it so that the triangle just covers the screen and has correct uvs. But, these can be generated in the vertex shader from the vertex index alone so no vertex buffer is needed, just a draw command drawing indices 0..3 and then the vertex shader clip position is output as:

out.clip_position = vec4<f32>(f32(in_vertex_index & 1u), f32(in_vertex_index >> 1u), 0.5, 0.5) * 4.0 - 1.0;

Untested at this time of writing but that should do it.

Copy link
Member

Choose a reason for hiding this comment

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

Can / should we add a explicit short hand for this? It seems like a common pattern.

Copy link
Contributor

@superdump superdump Jun 5, 2022

Choose a reason for hiding this comment

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

I suggested on Discord that we add a fullscreen pass node that can have its pipeline’s fragment shader stage and bind groups specialised, and that requires no mesh entity.

I think for this PR just making a triangle mesh with index buffer containing 0,1,2, overriding the vertex stage with the above and calculating the uvs in the fragment stage should do it.

A note on the why - I’ve seen articles about gaining significant % performance from using a fullscreen triangle or compute shader because a quad has two triangles, each with a diagonal cutting through the screen which causes duplicate fragment shader invocations because of fragments being shaded in 2x2 pixel quads which overlap the boundary. Maybe msaa also gets involved and makes the situation worse.

Copy link
Member

Choose a reason for hiding this comment

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

Sounds good. We can get this in, and then watch it improve.

Copy link
Member Author

Choose a reason for hiding this comment

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

Should I try this solution ? And try to benchmark the differences ? Maybe a different PR to make it clear we're not sure the added boilerplate is worth it (if we don't have shortcut available in engine).

Or would it be considered non blocking, add a documentation line about this concept, create an issue for future improvement ?

Copy link
Member

Choose a reason for hiding this comment

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

My personal feeling is that this is nonblocking, and just needs an issue opened.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm fine with that approach. If it were for internal bevy post-processing, I would block on it. But for an example, it's good enough.

material: material_handle,
transform: Transform {
translation: Vec3::new(0.0, 0.0, 1.5),
..default()
},
..default()
})
.insert(post_processing_pass_layer);

// The post-processing pass camera.
commands
.spawn_bundle(Camera2dBundle {
camera: Camera {
// render after the "main pass" camera
priority: 1,
..default()
},
..Camera2dBundle::default()
})
.insert(PostProcessingPassCamera)
.insert(post_processing_pass_layer);
}

/// Rotates the inner cube (main pass)
fn main_pass_cube_rotator_system(
time: Res<Time>,
mut query: Query<&mut Transform, With<MainPassCube>>,
) {
for mut transform in query.iter_mut() {
transform.rotation *= Quat::from_rotation_x(1.5 * time.delta_seconds());
Vrixyz marked this conversation as resolved.
Show resolved Hide resolved
transform.rotation *= Quat::from_rotation_z(1.3 * time.delta_seconds());
Vrixyz marked this conversation as resolved.
Show resolved Hide resolved
Vrixyz marked this conversation as resolved.
Show resolved Hide resolved
}
}

// Region below declares of the custom material handling post processing effect

/// Our custom post processing material
#[derive(TypeUuid, Clone)]
#[uuid = "bc2f08eb-a0fb-43f1-a908-54871ea597d5"]
struct PostProcessingMaterial {
/// In this example, this image will be the result of the main pass.
source_image: Handle<Image>,
}

struct PostProcessingMaterialGPU {
bind_group: BindGroup,
}

impl Material2d for PostProcessingMaterial {
fn bind_group(material: &PostProcessingMaterialGPU) -> &BindGroup {
&material.bind_group
}

fn bind_group_layout(render_device: &RenderDevice) -> BindGroupLayout {
render_device.create_bind_group_layout(&BindGroupLayoutDescriptor {
label: None,
entries: &[
BindGroupLayoutEntry {
binding: 0,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Texture {
multisampled: false,
view_dimension: TextureViewDimension::D2,
sample_type: TextureSampleType::Float { filterable: true },
},
count: None,
},
BindGroupLayoutEntry {
binding: 1,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Sampler(SamplerBindingType::Filtering),
count: None,
},
],
})
}

fn fragment_shader(asset_server: &AssetServer) -> Option<Handle<Shader>> {
asset_server.watch_for_changes().unwrap();
Some(asset_server.load("shaders/custom_material_chromatic_aberration.wgsl"))
}
}

impl RenderAsset for PostProcessingMaterial {
type ExtractedAsset = PostProcessingMaterial;
type PreparedAsset = PostProcessingMaterialGPU;
type Param = (
SRes<RenderDevice>,
SRes<Material2dPipeline<PostProcessingMaterial>>,
SRes<RenderAssets<Image>>,
);

fn prepare_asset(
extracted_asset: PostProcessingMaterial,
(render_device, pipeline, images): &mut SystemParamItem<Self::Param>,
) -> Result<PostProcessingMaterialGPU, PrepareAssetError<PostProcessingMaterial>> {
let (view, sampler) = if let Some(result) = pipeline
.mesh2d_pipeline
.get_image_texture(images, &Some(extracted_asset.source_image.clone()))
{
result
} else {
return Err(PrepareAssetError::RetryNextUpdate(extracted_asset));
};

let bind_group = render_device.create_bind_group(&BindGroupDescriptor {
label: None,
layout: &pipeline.material2d_layout,
entries: &[
BindGroupEntry {
binding: 0,
resource: BindingResource::TextureView(view),
},
BindGroupEntry {
binding: 1,
resource: BindingResource::Sampler(sampler),
},
],
});
Ok(PostProcessingMaterialGPU { bind_group })
}

fn extract_asset(&self) -> PostProcessingMaterial {
self.clone()
}
}