1use 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
27const SHADER_ASSET_PATH: &str = "shaders/post_processing.wgsl";
29
30fn 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
38struct PostProcessPlugin;
40
41impl Plugin for PostProcessPlugin {
42 fn build(&self, app: &mut App) {
43 app.add_plugins((
44 ExtractComponentPlugin::<PostProcessSettings>::default(),
51 UniformComponentPlugin::<PostProcessSettings>::default(),
55 ));
56
57 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)]
71struct PostProcessBindGroupCache {
72 cached: Option<(TextureViewId, BindGroup)>,
73}
74
75fn 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 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 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 &BindGroupEntries::sequential((
126 post_process.source,
128 &post_process_pipeline.sampler,
130 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 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 render_pass.set_bind_group(0, bind_group, &[settings_index.index()]);
163 render_pass.draw(0..3, 0..1);
164}
165
166#[derive(Resource)]
168struct PostProcessPipeline {
169 layout: BindGroupLayoutDescriptor,
170 sampler: Sampler,
171 pipeline_id: CachedRenderPipelineId,
172}
173
174fn 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 let layout = BindGroupLayoutDescriptor::new(
183 "post_process_bind_group_layout",
184 &BindGroupLayoutEntries::sequential(
185 ShaderStages::FRAGMENT,
187 (
188 texture_2d(TextureSampleType::Float { filterable: true }),
190 sampler(SamplerBindingType::Filtering),
192 uniform_buffer::<PostProcessSettings>(true),
194 ),
195 ),
196 );
197 let sampler = render_device.create_sampler(&SamplerDescriptor::default());
199
200 let shader = asset_server.load(SHADER_ASSET_PATH);
202 let vertex_state = fullscreen_shader.to_vertex_state();
204 let pipeline_id = pipeline_cache
205 .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 targets: vec![Some(ColorTargetState {
215 format: TextureFormat::Rgba8UnormSrgb,
216 blend: None,
217 write_mask: ColorWrites::ALL,
218 })],
219 ..default()
220 }),
221 ..default()
222 });
223 commands.insert_resource(PostProcessPipeline {
224 layout,
225 sampler,
226 pipeline_id,
227 });
228}
229
230#[derive(Component, Default, Clone, Copy, ExtractComponent, ShaderType)]
232struct PostProcessSettings {
233 intensity: f32,
234 #[cfg(feature = "webgl2")]
236 _webgl2_padding: Vec3,
237}
238
239fn setup(
241 mut commands: Commands,
242 mut meshes: ResMut<Assets<Mesh>>,
243 mut materials: ResMut<Assets<StandardMaterial>>,
244) {
245 commands.spawn((
249 Camera3d::default(),
250 Transform::from_translation(Vec3::new(0.0, 0.0, 5.0)).looking_at(Vec3::default(), Vec3::Y),
251 Camera {
252 clear_color: Color::WHITE.into(),
253 ..default()
254 },
255 PostProcessSettings {
258 intensity: 0.02,
259 ..default()
260 },
261 ));
262
263 commands.spawn((
265 Mesh3d(meshes.add(Cuboid::default())),
266 MeshMaterial3d(materials.add(Color::srgb(0.8, 0.7, 0.6))),
267 Transform::from_xyz(0.0, 0.5, 0.0),
268 Rotates,
269 ));
270 commands.spawn(DirectionalLight {
272 illuminance: 1_000.,
273 ..default()
274 });
275}
276
277#[derive(Component)]
278struct Rotates;
279
280fn rotate(time: Res<Time>, mut query: Query<&mut Transform, With<Rotates>>) {
282 for mut transform in &mut query {
283 transform.rotate_x(0.55 * time.delta_secs());
284 transform.rotate_z(0.15 * time.delta_secs());
285 }
286}
287
288fn update_settings(mut settings: Query<&mut PostProcessSettings>, time: Res<Time>) {
290 for mut setting in &mut settings {
291 let mut intensity = ops::sin(time.elapsed_secs());
292 intensity = ops::sin(intensity);
294 intensity = intensity * 0.5 + 0.5;
296 intensity *= 0.015;
298
299 setting.intensity = intensity;
302 }
303}