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 Render, RenderApp, RenderStartup, RenderSystems,
24 },
25};
26
27const SHADER_ASSET_PATH: &str = "shaders/post_processing.wesl";
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
63 .add_systems(RenderStartup, init_post_process_pipeline)
64 .add_systems(
65 Render,
66 prepare_bind_groups.in_set(RenderSystems::PrepareBindGroups),
67 )
68 .add_systems(
69 Core3d,
70 post_process_system.in_set(Core3dSystems::PostProcess),
71 );
72 }
73}
74
75#[derive(Component)]
80struct PostProcessBindGroups {
81 a: (TextureViewId, BindGroup),
82 b: (TextureViewId, BindGroup),
83}
84
85fn prepare_bind_groups(
89 mut commands: Commands,
90 mut views: Query<(Entity, &ViewTarget, Option<&mut PostProcessBindGroups>)>,
91 post_process_pipeline: Option<Res<PostProcessPipeline>>,
92 pipeline_cache: Res<PipelineCache>,
93 settings_uniforms: Res<ComponentUniforms<PostProcessSettings>>,
94 render_device: Res<RenderDevice>,
95) {
96 let Some(post_process_pipeline) = post_process_pipeline else {
97 return;
98 };
99 let Some(settings_binding) = settings_uniforms.uniforms().binding() else {
100 return;
101 };
102
103 let create_bind_group = |texture: &TextureView| {
104 (
105 texture.id(),
106 render_device.create_bind_group(
107 "post_process_bind_group",
108 &pipeline_cache.get_bind_group_layout(&post_process_pipeline.layout),
109 &BindGroupEntries::sequential((
110 texture,
111 &post_process_pipeline.sampler,
112 settings_binding.clone(),
113 )),
114 ),
115 )
116 };
117
118 for (entity, view_target, mut maybe_bind_groups) in &mut views {
119 let main_texture_view = view_target.main_texture_view();
120 let main_texture_other_view = view_target.main_texture_other_view();
121
122 if let Some(bind_groups) = &mut maybe_bind_groups {
124 if bind_groups.a.0 != main_texture_view.id() {
125 bind_groups.a = create_bind_group(main_texture_view);
126 }
127 if bind_groups.b.0 != main_texture_other_view.id() {
128 bind_groups.b = create_bind_group(main_texture_other_view);
129 }
130 } else {
131 commands.entity(entity).insert(PostProcessBindGroups {
133 a: create_bind_group(main_texture_view),
134 b: create_bind_group(main_texture_other_view),
135 });
136 }
137 }
138}
139
140fn post_process_system(
141 view: ViewQuery<(
142 &ViewTarget,
143 &DynamicUniformIndex<PostProcessSettings>,
144 &PostProcessBindGroups,
145 )>,
146 post_process_pipeline: Option<Res<PostProcessPipeline>>,
147 pipeline_cache: Res<PipelineCache>,
148 mut ctx: RenderContext,
149) {
150 let Some(post_process_pipeline) = post_process_pipeline else {
151 return;
152 };
153
154 let (view_target, settings_index, bind_groups) = view.into_inner();
155
156 let Some(pipeline) = pipeline_cache.get_render_pipeline(post_process_pipeline.pipeline_id)
157 else {
158 return;
159 };
160
161 let post_process = view_target.post_process_write();
169
170 let (_, bind_group) = if bind_groups.a.0 == post_process.source.id() {
173 &bind_groups.a
174 } else {
175 &bind_groups.b
176 };
177
178 let mut render_pass = ctx
179 .command_encoder()
180 .begin_render_pass(&RenderPassDescriptor {
181 label: Some("post_process_pass"),
182 color_attachments: &[Some(RenderPassColorAttachment {
183 view: post_process.destination,
186 depth_slice: None,
187 resolve_target: None,
188 ops: Operations::default(),
189 })],
190 depth_stencil_attachment: None,
191 timestamp_writes: None,
192 occlusion_query_set: None,
193 multiview_mask: None,
194 });
195
196 render_pass.set_pipeline(pipeline);
197 render_pass.set_bind_group(0, bind_group, &[settings_index.index()]);
201 render_pass.draw(0..3, 0..1);
202}
203
204#[derive(Resource)]
206struct PostProcessPipeline {
207 layout: BindGroupLayoutDescriptor,
208 sampler: Sampler,
209 pipeline_id: CachedRenderPipelineId,
210}
211
212fn init_post_process_pipeline(
213 mut commands: Commands,
214 render_device: Res<RenderDevice>,
215 asset_server: Res<AssetServer>,
216 fullscreen_shader: Res<FullscreenShader>,
217 pipeline_cache: Res<PipelineCache>,
218) {
219 let layout = BindGroupLayoutDescriptor::new(
221 "post_process_bind_group_layout",
222 &BindGroupLayoutEntries::sequential(
223 ShaderStages::FRAGMENT,
225 (
226 texture_2d(TextureSampleType::Float { filterable: true }),
228 sampler(SamplerBindingType::Filtering),
230 uniform_buffer::<PostProcessSettings>(true),
232 ),
233 ),
234 );
235 let sampler = render_device.create_sampler(&SamplerDescriptor::default());
237
238 let shader = asset_server.load(SHADER_ASSET_PATH);
240 let vertex_state = fullscreen_shader.to_vertex_state();
242 let pipeline_id = pipeline_cache
243 .queue_render_pipeline(RenderPipelineDescriptor {
245 label: Some("post_process_pipeline".into()),
246 layout: vec![layout.clone()],
247 vertex: vertex_state,
248 fragment: Some(FragmentState {
249 shader,
250 targets: vec![Some(ColorTargetState {
253 format: TextureFormat::Rgba8UnormSrgb,
254 blend: None,
255 write_mask: ColorWrites::ALL,
256 })],
257 ..default()
258 }),
259 ..default()
260 });
261 commands.insert_resource(PostProcessPipeline {
262 layout,
263 sampler,
264 pipeline_id,
265 });
266}
267
268#[derive(Component, Default, Clone, Copy, ExtractComponent, ShaderType)]
270#[extract_app(RenderApp)]
271struct PostProcessSettings {
272 intensity: f32,
273 #[cfg(feature = "webgl2")]
275 _webgl2_padding: Vec3,
276}
277
278fn setup(
280 mut commands: Commands,
281 mut meshes: ResMut<Assets<Mesh>>,
282 mut materials: ResMut<Assets<StandardMaterial>>,
283) {
284 commands.spawn((
288 Camera3d::default(),
289 Transform::from_translation(Vec3::new(0.0, 0.0, 5.0)).looking_at(Vec3::default(), Vec3::Y),
290 Camera {
291 clear_color: Color::WHITE.into(),
292 ..default()
293 },
294 PostProcessSettings {
297 intensity: 0.02,
298 ..default()
299 },
300 ));
301
302 commands.spawn((
304 Mesh3d(meshes.add(Cuboid::default())),
305 MeshMaterial3d(materials.add(Color::srgb(0.8, 0.7, 0.6))),
306 Transform::from_xyz(0.0, 0.5, 0.0),
307 Rotates,
308 ));
309 commands.spawn(DirectionalLight {
311 illuminance: 1_000.,
312 ..default()
313 });
314}
315
316#[derive(Component)]
317struct Rotates;
318
319fn rotate(time: Res<Time>, mut query: Query<&mut Transform, With<Rotates>>) {
321 for mut transform in &mut query {
322 transform.rotate_x(0.55 * time.delta_secs());
323 transform.rotate_z(0.15 * time.delta_secs());
324 }
325}
326
327fn update_settings(mut settings: Query<&mut PostProcessSettings>, time: Res<Time>) {
329 for mut setting in &mut settings {
330 let mut intensity = ops::sin(time.elapsed_secs());
331 intensity = ops::sin(intensity);
333 intensity = intensity * 0.5 + 0.5;
335 intensity *= 0.015;
337
338 setting.intensity = intensity;
341 }
342}