Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/examples/shader_advanced/custom_post_processing.rs
9341 views
1
//! This example shows how to create a custom post-processing effect that runs after the main pass
2
//! and reads the texture generated by the main pass.
3
//!
4
//! The example shader is a very simple implementation of chromatic aberration.
5
//! To adapt this example for 2D, replace all instances of 3D structures (such as `Core3d`, etc.) with their corresponding 2D counterparts.
6
//!
7
//! This is a fairly low level example and assumes some familiarity with rendering concepts and wgpu.
8
9
use bevy::{
10
core_pipeline::{schedule::Core3d, Core3dSystems, FullscreenShader},
11
prelude::*,
12
render::{
13
extract_component::{
14
ComponentUniforms, DynamicUniformIndex, ExtractComponent, ExtractComponentPlugin,
15
UniformComponentPlugin,
16
},
17
render_resource::{
18
binding_types::{sampler, texture_2d, uniform_buffer},
19
*,
20
},
21
renderer::{RenderContext, RenderDevice, ViewQuery},
22
view::ViewTarget,
23
RenderApp, RenderStartup,
24
},
25
};
26
27
/// This example uses a shader source file from the assets subdirectory
28
const SHADER_ASSET_PATH: &str = "shaders/post_processing.wgsl";
29
30
fn main() {
31
App::new()
32
.add_plugins((DefaultPlugins, PostProcessPlugin))
33
.add_systems(Startup, setup)
34
.add_systems(Update, (rotate, update_settings))
35
.run();
36
}
37
38
/// It is generally encouraged to set up post processing effects as a plugin
39
struct PostProcessPlugin;
40
41
impl Plugin for PostProcessPlugin {
42
fn build(&self, app: &mut App) {
43
app.add_plugins((
44
// The settings will be a component that lives in the main world but will
45
// be extracted to the render world every frame.
46
// This makes it possible to control the effect from the main world.
47
// This plugin will take care of extracting it automatically.
48
// It's important to derive [`ExtractComponent`] on [`PostProcessSettings`]
49
// for this plugin to work correctly.
50
ExtractComponentPlugin::<PostProcessSettings>::default(),
51
// The settings will also be the data used in the shader.
52
// This plugin will prepare the component for the GPU by creating a uniform buffer
53
// and writing the data to that buffer every frame.
54
UniformComponentPlugin::<PostProcessSettings>::default(),
55
));
56
57
// We need to get the render app from the main app
58
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
59
return;
60
};
61
62
render_app.add_systems(RenderStartup, init_post_process_pipeline);
63
render_app.add_systems(
64
Core3d,
65
post_process_system.in_set(Core3dSystems::PostProcess),
66
);
67
}
68
}
69
70
#[derive(Default)]
71
struct PostProcessBindGroupCache {
72
cached: Option<(TextureViewId, BindGroup)>,
73
}
74
75
fn post_process_system(
76
view: ViewQuery<(
77
&ViewTarget,
78
&PostProcessSettings,
79
&DynamicUniformIndex<PostProcessSettings>,
80
)>,
81
post_process_pipeline: Option<Res<PostProcessPipeline>>,
82
pipeline_cache: Res<PipelineCache>,
83
settings_uniforms: Res<ComponentUniforms<PostProcessSettings>>,
84
mut cache: Local<PostProcessBindGroupCache>,
85
mut ctx: RenderContext,
86
) {
87
let Some(post_process_pipeline) = post_process_pipeline else {
88
return;
89
};
90
91
let (view_target, _post_process_settings, settings_index) = view.into_inner();
92
93
let Some(pipeline) = pipeline_cache.get_render_pipeline(post_process_pipeline.pipeline_id)
94
else {
95
return;
96
};
97
98
let Some(settings_binding) = settings_uniforms.uniforms().binding() else {
99
return;
100
};
101
102
// This will start a new "post process write", obtaining two texture
103
// views from the view target - a `source` and a `destination`.
104
// `source` is the "current" main texture and you _must_ write into
105
// `destination` because calling `post_process_write()` on the
106
// [`ViewTarget`] will internally flip the [`ViewTarget`]'s main
107
// texture to the `destination` texture. Failing to do so will cause
108
// the current main texture information to be lost.
109
let post_process = view_target.post_process_write();
110
111
let bind_group = match &mut cache.cached {
112
Some((texture_id, bind_group)) if post_process.source.id() == *texture_id => bind_group,
113
cached => {
114
// The bind_group gets created each frame.
115
//
116
// Normally, you would create a bind_group in the Queue set,
117
// but this doesn't work with the post_process_write().
118
// The reason it doesn't work is because each post_process_write will alternate the source/destination.
119
// The only way to have the correct source/destination for the bind_group
120
// is to make sure you get it during the node execution.
121
let bind_group = ctx.render_device().create_bind_group(
122
"post_process_bind_group",
123
&pipeline_cache.get_bind_group_layout(&post_process_pipeline.layout),
124
// It's important for this to match the BindGroupLayout defined in the PostProcessPipeline
125
&BindGroupEntries::sequential((
126
// Make sure to use the source view
127
post_process.source,
128
// Use the sampler created for the pipeline
129
&post_process_pipeline.sampler,
130
// Set the settings binding
131
settings_binding.clone(),
132
)),
133
);
134
135
let (_, bind_group) = cached.insert((post_process.source.id(), bind_group));
136
bind_group
137
}
138
};
139
140
let mut render_pass = ctx
141
.command_encoder()
142
.begin_render_pass(&RenderPassDescriptor {
143
label: Some("post_process_pass"),
144
color_attachments: &[Some(RenderPassColorAttachment {
145
// We need to specify the post process destination view here
146
// to make sure we write to the appropriate texture.
147
view: post_process.destination,
148
depth_slice: None,
149
resolve_target: None,
150
ops: Operations::default(),
151
})],
152
depth_stencil_attachment: None,
153
timestamp_writes: None,
154
occlusion_query_set: None,
155
multiview_mask: None,
156
});
157
158
render_pass.set_pipeline(pipeline);
159
// By passing in the index of the post process settings on this view, we ensure
160
// that in the event that multiple settings were sent to the GPU (as would be the
161
// case with multiple cameras), we use the correct one.
162
render_pass.set_bind_group(0, bind_group, &[settings_index.index()]);
163
render_pass.draw(0..3, 0..1);
164
}
165
166
// This contains global data used by the render pipeline. This will be created once on startup.
167
#[derive(Resource)]
168
struct PostProcessPipeline {
169
layout: BindGroupLayoutDescriptor,
170
sampler: Sampler,
171
pipeline_id: CachedRenderPipelineId,
172
}
173
174
fn init_post_process_pipeline(
175
mut commands: Commands,
176
render_device: Res<RenderDevice>,
177
asset_server: Res<AssetServer>,
178
fullscreen_shader: Res<FullscreenShader>,
179
pipeline_cache: Res<PipelineCache>,
180
) {
181
// We need to define the bind group layout used for our pipeline
182
let layout = BindGroupLayoutDescriptor::new(
183
"post_process_bind_group_layout",
184
&BindGroupLayoutEntries::sequential(
185
// The layout entries will only be visible in the fragment stage
186
ShaderStages::FRAGMENT,
187
(
188
// The screen texture
189
texture_2d(TextureSampleType::Float { filterable: true }),
190
// The sampler that will be used to sample the screen texture
191
sampler(SamplerBindingType::Filtering),
192
// The settings uniform that will control the effect
193
uniform_buffer::<PostProcessSettings>(true),
194
),
195
),
196
);
197
// We can create the sampler here since it won't change at runtime and doesn't depend on the view
198
let sampler = render_device.create_sampler(&SamplerDescriptor::default());
199
200
// Get the shader handle
201
let shader = asset_server.load(SHADER_ASSET_PATH);
202
// This will setup a fullscreen triangle for the vertex state.
203
let vertex_state = fullscreen_shader.to_vertex_state();
204
let pipeline_id = pipeline_cache
205
// This will add the pipeline to the cache and queue its creation
206
.queue_render_pipeline(RenderPipelineDescriptor {
207
label: Some("post_process_pipeline".into()),
208
layout: vec![layout.clone()],
209
vertex: vertex_state,
210
fragment: Some(FragmentState {
211
shader,
212
// Make sure this matches the entry point of your shader.
213
// It can be anything as long as it matches here and in the shader.
214
// Use `format: ViewTarget::TEXTURE_FORMAT_HDR` for HDR cameras.
215
targets: vec![Some(ColorTargetState {
216
format: TextureFormat::bevy_default(),
217
blend: None,
218
write_mask: ColorWrites::ALL,
219
})],
220
..default()
221
}),
222
..default()
223
});
224
commands.insert_resource(PostProcessPipeline {
225
layout,
226
sampler,
227
pipeline_id,
228
});
229
}
230
231
// This is the component that will get passed to the shader
232
#[derive(Component, Default, Clone, Copy, ExtractComponent, ShaderType)]
233
struct PostProcessSettings {
234
intensity: f32,
235
// WebGL2 structs must be 16 byte aligned.
236
#[cfg(feature = "webgl2")]
237
_webgl2_padding: Vec3,
238
}
239
240
/// Set up a simple 3D scene
241
fn setup(
242
mut commands: Commands,
243
mut meshes: ResMut<Assets<Mesh>>,
244
mut materials: ResMut<Assets<StandardMaterial>>,
245
) {
246
// camera
247
// Make sure you change the TextureFormat of the ColorTargetState
248
// if you enable Hdr directly or through features like Bloom.
249
commands.spawn((
250
Camera3d::default(),
251
Transform::from_translation(Vec3::new(0.0, 0.0, 5.0)).looking_at(Vec3::default(), Vec3::Y),
252
Camera {
253
clear_color: Color::WHITE.into(),
254
..default()
255
},
256
// Add the setting to the camera.
257
// This component is also used to determine on which camera to run the post processing effect.
258
PostProcessSettings {
259
intensity: 0.02,
260
..default()
261
},
262
));
263
264
// cube
265
commands.spawn((
266
Mesh3d(meshes.add(Cuboid::default())),
267
MeshMaterial3d(materials.add(Color::srgb(0.8, 0.7, 0.6))),
268
Transform::from_xyz(0.0, 0.5, 0.0),
269
Rotates,
270
));
271
// light
272
commands.spawn(DirectionalLight {
273
illuminance: 1_000.,
274
..default()
275
});
276
}
277
278
#[derive(Component)]
279
struct Rotates;
280
281
/// Rotates any entity around the x and y axis
282
fn rotate(time: Res<Time>, mut query: Query<&mut Transform, With<Rotates>>) {
283
for mut transform in &mut query {
284
transform.rotate_x(0.55 * time.delta_secs());
285
transform.rotate_z(0.15 * time.delta_secs());
286
}
287
}
288
289
// Change the intensity over time to show that the effect is controlled from the main world
290
fn update_settings(mut settings: Query<&mut PostProcessSettings>, time: Res<Time>) {
291
for mut setting in &mut settings {
292
let mut intensity = ops::sin(time.elapsed_secs());
293
// Make it loop periodically
294
intensity = ops::sin(intensity);
295
// Remap it to 0..1 because the intensity can't be negative
296
intensity = intensity * 0.5 + 0.5;
297
// Scale it to a more reasonable level
298
intensity *= 0.015;
299
300
// Set the intensity.
301
// This will then be extracted to the render world and uploaded to the GPU automatically by the [`UniformComponentPlugin`]
302
setting.intensity = intensity;
303
}
304
}
305
306