bevy/examples/shader/post_processing.rs
Cameron 01649f13e2
Refactor App and SubApp internals for better separation (#9202)
# Objective

This is a necessary precursor to #9122 (this was split from that PR to
reduce the amount of code to review all at once).

Moving `!Send` resource ownership to `App` will make it unambiguously
`!Send`. `SubApp` must be `Send`, so it can't wrap `App`.

## Solution

Refactor `App` and `SubApp` to not have a recursive relationship. Since
`SubApp` no longer wraps `App`, once `!Send` resources are moved out of
`World` and into `App`, `SubApp` will become unambiguously `Send`.

There could be less code duplication between `App` and `SubApp`, but
that would break `App` method chaining.

## Changelog

- `SubApp` no longer wraps `App`.
- `App` fields are no longer publicly accessible.
- `App` can no longer be converted into a `SubApp`.
- Various methods now return references to a `SubApp` instead of an
`App`.
## Migration Guide

- To construct a sub-app, use `SubApp::new()`. `App` can no longer
convert into `SubApp`.
- If you implemented a trait for `App`, you may want to implement it for
`SubApp` as well.
- If you're accessing `app.world` directly, you now have to use
`app.world()` and `app.world_mut()`.
- `App::sub_app` now returns `&SubApp`.
- `App::sub_app_mut`  now returns `&mut SubApp`.
- `App::get_sub_app` now returns `Option<&SubApp>.`
- `App::get_sub_app_mut` now returns `Option<&mut SubApp>.`
2024-03-31 03:16:10 +00:00

370 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, 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,
},
};
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,
);
// 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): 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);
render_pass.set_bind_group(0, &bind_group, &[]);
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>(false),
),
),
);
// 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("shaders/post_processing.wgsl");
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;
}
}