1mod chromatic_aberration;
10mod lens_distortion;
11mod vignette;
12
13use bevy_color::ColorToComponents;
14pub use chromatic_aberration::{ChromaticAberration, ChromaticAberrationUniform};
15pub use lens_distortion::{LensDistortion, LensDistortionUniform};
16pub use vignette::{Vignette, VignetteUniform};
17
18use crate::effect_stack::chromatic_aberration::{
19 DefaultChromaticAberrationLut, DEFAULT_CHROMATIC_ABERRATION_LUT_DATA,
20};
21
22use bevy_app::{App, Plugin};
23use bevy_asset::{
24 embedded_asset, load_embedded_asset, AssetServer, Assets, Handle, RenderAssetUsages,
25};
26use bevy_derive::{Deref, DerefMut};
27use bevy_ecs::{
28 component::Component,
29 entity::Entity,
30 query::{AnyOf, Or, With},
31 resource::Resource,
32 schedule::IntoScheduleConfigs as _,
33 system::{Commands, Query, Res, ResMut},
34};
35use bevy_image::Image;
36use bevy_render::{
37 camera::ExtractedCamera,
38 diagnostic::RecordDiagnostics,
39 extract_component::ExtractComponentPlugin,
40 render_asset::RenderAssets,
41 render_resource::{
42 binding_types::{sampler, texture_2d, uniform_buffer},
43 BindGroupEntries, BindGroupLayoutDescriptor, BindGroupLayoutEntries,
44 CachedRenderPipelineId, ColorTargetState, ColorWrites, DynamicUniformBuffer, Extent3d,
45 FilterMode, FragmentState, MipmapFilterMode, Operations, PipelineCache,
46 RenderPassColorAttachment, RenderPassDescriptor, RenderPipelineDescriptor, Sampler,
47 SamplerBindingType, SamplerDescriptor, ShaderStages, SpecializedRenderPipeline,
48 SpecializedRenderPipelines, TextureDimension, TextureFormat, TextureSampleType,
49 },
50 renderer::{RenderContext, RenderDevice, RenderQueue, ViewQuery},
51 texture::GpuImage,
52 view::{ExtractedView, ViewTarget},
53 GpuResourceAppExt, Render, RenderApp, RenderStartup, RenderSystems,
54};
55use bevy_shader::{load_shader_library, Shader};
56use bevy_utils::prelude::default;
57
58use crate::{bloom::bloom, dof::depth_of_field};
59use bevy_core_pipeline::{
60 schedule::{Core2d, Core3d},
61 tonemapping::tonemapping,
62 FullscreenShader,
63};
64
65#[derive(Default)]
74pub struct EffectStackPlugin;
75
76#[derive(Resource)]
80pub struct PostProcessingPipeline {
81 bind_group_layout: BindGroupLayoutDescriptor,
83 common_sampler: Sampler,
85 fullscreen_shader: FullscreenShader,
87 fragment_shader: Handle<Shader>,
89}
90
91#[derive(Clone, Copy, PartialEq, Eq, Hash)]
93pub struct PostProcessingPipelineKey {
94 target_format: TextureFormat,
96}
97
98#[derive(Component, Deref, DerefMut)]
101pub struct PostProcessingPipelineId(CachedRenderPipelineId);
102
103#[derive(Resource, Default)]
109pub struct PostProcessingUniformBuffers {
110 chromatic_aberration: DynamicUniformBuffer<ChromaticAberrationUniform>,
111 vignette: DynamicUniformBuffer<VignetteUniform>,
112 lens_distortion: DynamicUniformBuffer<LensDistortionUniform>,
113}
114
115#[derive(Component)]
119pub struct PostProcessingUniformBufferOffsets {
120 chromatic_aberration: u32,
121 vignette: u32,
122 lens_distortion: u32,
123}
124
125impl Plugin for EffectStackPlugin {
126 fn build(&self, app: &mut App) {
127 load_shader_library!(app, "chromatic_aberration.wgsl");
128 load_shader_library!(app, "lens_distortion.wgsl");
129 load_shader_library!(app, "vignette.wgsl");
130
131 embedded_asset!(app, "post_process.wgsl");
132
133 let mut assets = app.world_mut().resource_mut::<Assets<_>>();
135 let default_lut = assets.add(Image::new(
136 Extent3d {
137 width: 3,
138 height: 1,
139 depth_or_array_layers: 1,
140 },
141 TextureDimension::D2,
142 DEFAULT_CHROMATIC_ABERRATION_LUT_DATA.to_vec(),
143 TextureFormat::Rgba8UnormSrgb,
144 RenderAssetUsages::RENDER_WORLD,
145 ));
146
147 app.add_plugins(ExtractComponentPlugin::<ChromaticAberration>::default())
148 .add_plugins(ExtractComponentPlugin::<LensDistortion>::default())
149 .add_plugins(ExtractComponentPlugin::<Vignette>::default());
150
151 let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
152 return;
153 };
154
155 render_app
156 .insert_resource(DefaultChromaticAberrationLut(default_lut))
157 .init_gpu_resource::<SpecializedRenderPipelines<PostProcessingPipeline>>()
158 .init_gpu_resource::<PostProcessingUniformBuffers>()
159 .add_systems(RenderStartup, init_post_processing_pipeline)
160 .add_systems(
161 Render,
162 (
163 prepare_post_processing_pipelines,
164 prepare_post_processing_uniforms,
165 )
166 .in_set(RenderSystems::Prepare),
167 )
168 .add_systems(
169 Core3d,
170 post_processing.after(depth_of_field).before(tonemapping),
171 )
172 .add_systems(Core2d, post_processing.after(bloom).before(tonemapping));
173 }
174}
175
176pub(crate) fn init_post_processing_pipeline(
177 mut commands: Commands,
178 render_device: Res<RenderDevice>,
179 fullscreen_shader: Res<FullscreenShader>,
180 asset_server: Res<AssetServer>,
181) {
182 let bind_group_layout = BindGroupLayoutDescriptor::new(
184 "postprocessing bind group layout",
185 &BindGroupLayoutEntries::sequential(
186 ShaderStages::FRAGMENT,
187 (
188 texture_2d(TextureSampleType::Float { filterable: true }),
190 sampler(SamplerBindingType::Filtering),
192 texture_2d(TextureSampleType::Float { filterable: true }),
194 uniform_buffer::<ChromaticAberrationUniform>(true),
196 uniform_buffer::<VignetteUniform>(true),
198 uniform_buffer::<LensDistortionUniform>(true),
200 ),
201 ),
202 );
203
204 let common_sampler = render_device.create_sampler(&SamplerDescriptor {
207 mipmap_filter: MipmapFilterMode::Linear,
208 min_filter: FilterMode::Linear,
209 mag_filter: FilterMode::Linear,
210 ..default()
211 });
212
213 commands.insert_resource(PostProcessingPipeline {
214 bind_group_layout,
215 common_sampler,
216 fullscreen_shader: fullscreen_shader.clone(),
217 fragment_shader: load_embedded_asset!(asset_server.as_ref(), "post_process.wgsl"),
218 });
219}
220
221impl SpecializedRenderPipeline for PostProcessingPipeline {
222 type Key = PostProcessingPipelineKey;
223
224 fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
225 RenderPipelineDescriptor {
226 label: Some("postprocessing".into()),
227 layout: vec![self.bind_group_layout.clone()],
228 vertex: self.fullscreen_shader.to_vertex_state(),
229 fragment: Some(FragmentState {
230 shader: self.fragment_shader.clone(),
231 targets: vec![Some(ColorTargetState {
232 format: key.target_format,
233 blend: None,
234 write_mask: ColorWrites::ALL,
235 })],
236 ..default()
237 }),
238 ..default()
239 }
240 }
241}
242
243pub(crate) fn post_processing(
244 view: ViewQuery<(
245 &ViewTarget,
246 &PostProcessingPipelineId,
247 AnyOf<(&ChromaticAberration, &Vignette, &LensDistortion)>,
248 &PostProcessingUniformBufferOffsets,
249 )>,
250 pipeline_cache: Res<PipelineCache>,
251 post_processing_pipeline: Res<PostProcessingPipeline>,
252 post_processing_uniform_buffers: Res<PostProcessingUniformBuffers>,
253 gpu_image_assets: Res<RenderAssets<GpuImage>>,
254 default_lut: Res<DefaultChromaticAberrationLut>,
255 mut ctx: RenderContext,
256) {
257 let (view_target, pipeline_id, post_effects, post_processing_uniform_buffer_offsets) =
258 view.into_inner();
259
260 let (maybe_chromatic_aberration, maybe_vignette, maybe_lens_distortion) = post_effects;
261
262 if maybe_chromatic_aberration.is_none()
263 && maybe_vignette.is_none()
264 && maybe_lens_distortion.is_none()
265 {
266 return;
267 }
268
269 let Some(pipeline) = pipeline_cache.get_render_pipeline(**pipeline_id) else {
271 return;
272 };
273
274 let Some(chromatic_aberration_lut) = gpu_image_assets.get(
276 maybe_chromatic_aberration
277 .and_then(|ca| ca.color_lut.as_ref())
278 .unwrap_or(&default_lut.0),
279 ) else {
280 return;
281 };
282
283 let Some(chromatic_aberration_uniform_buffer_binding) = post_processing_uniform_buffers
285 .chromatic_aberration
286 .binding()
287 else {
288 return;
289 };
290
291 let Some(vignette_uniform_buffer_binding) = post_processing_uniform_buffers.vignette.binding()
292 else {
293 return;
294 };
295
296 let Some(lens_distortion_uniform_buffer_binding) =
297 post_processing_uniform_buffers.lens_distortion.binding()
298 else {
299 return;
300 };
301
302 let post_process = view_target.post_process_write();
304
305 let pass_descriptor = RenderPassDescriptor {
306 label: Some("postprocessing"),
307 color_attachments: &[Some(RenderPassColorAttachment {
308 view: post_process.destination,
309 depth_slice: None,
310 resolve_target: None,
311 ops: Operations::default(),
312 })],
313 depth_stencil_attachment: None,
314 timestamp_writes: None,
315 occlusion_query_set: None,
316 multiview_mask: None,
317 };
318
319 let bind_group = ctx.render_device().create_bind_group(
320 Some("postprocessing bind group"),
321 &pipeline_cache.get_bind_group_layout(&post_processing_pipeline.bind_group_layout),
322 &BindGroupEntries::sequential((
323 post_process.source,
324 &post_processing_pipeline.common_sampler,
325 &chromatic_aberration_lut.texture_view,
326 chromatic_aberration_uniform_buffer_binding,
327 vignette_uniform_buffer_binding,
328 lens_distortion_uniform_buffer_binding,
329 )),
330 );
331
332 let diagnostics = ctx.diagnostic_recorder();
333 let diagnostics = diagnostics.as_deref();
334
335 let mut render_pass = ctx.begin_tracked_render_pass(pass_descriptor);
336 let pass_span = diagnostics.pass_span(&mut render_pass, "postprocessing");
337
338 render_pass.set_render_pipeline(pipeline);
339 render_pass.set_bind_group(
340 0,
341 &bind_group,
342 &[
343 post_processing_uniform_buffer_offsets.chromatic_aberration,
344 post_processing_uniform_buffer_offsets.vignette,
345 post_processing_uniform_buffer_offsets.lens_distortion,
346 ],
347 );
348 render_pass.draw(0..3, 0..1);
349
350 pass_span.end(&mut render_pass);
351}
352
353pub(crate) fn prepare_post_processing_pipelines(
355 mut commands: Commands,
356 pipeline_cache: Res<PipelineCache>,
357 mut pipelines: ResMut<SpecializedRenderPipelines<PostProcessingPipeline>>,
358 post_processing_pipeline: Res<PostProcessingPipeline>,
359 cameras: Query<
360 (Entity, &ExtractedView),
361 Or<(
362 With<ChromaticAberration>,
363 With<Vignette>,
364 With<LensDistortion>,
365 With<ExtractedCamera>,
366 )>,
367 >,
368) {
369 for (entity, view) in cameras.iter() {
370 let pipeline_id = pipelines.specialize(
371 &pipeline_cache,
372 &post_processing_pipeline,
373 PostProcessingPipelineKey {
374 target_format: view.target_format,
375 },
376 );
377
378 commands
379 .entity(entity)
380 .insert(PostProcessingPipelineId(pipeline_id));
381 }
382}
383
384pub(crate) fn prepare_post_processing_uniforms(
387 mut commands: Commands,
388 mut post_processing_uniform_buffers: ResMut<PostProcessingUniformBuffers>,
389 render_device: Res<RenderDevice>,
390 render_queue: Res<RenderQueue>,
391 mut views: Query<
392 (
393 Entity,
394 Option<&ChromaticAberration>,
395 Option<&Vignette>,
396 Option<&LensDistortion>,
397 ),
398 Or<(
399 With<ChromaticAberration>,
400 With<Vignette>,
401 With<LensDistortion>,
402 )>,
403 >,
404) {
405 post_processing_uniform_buffers.chromatic_aberration.clear();
406 post_processing_uniform_buffers.vignette.clear();
407 post_processing_uniform_buffers.lens_distortion.clear();
408
409 for (view_entity, maybe_chromatic_aberration, maybe_vignette, maybe_lens_distortion) in
411 views.iter_mut()
412 {
413 let chromatic_aberration_uniform_buffer_offset =
414 if let Some(chromatic_aberration) = maybe_chromatic_aberration {
415 post_processing_uniform_buffers.chromatic_aberration.push(
416 &ChromaticAberrationUniform {
417 intensity: chromatic_aberration.intensity,
418 max_samples: chromatic_aberration.max_samples,
419 unused_1: 0,
420 unused_2: 0,
421 },
422 )
423 } else {
424 post_processing_uniform_buffers
425 .chromatic_aberration
426 .push(&ChromaticAberrationUniform::default())
427 };
428
429 let vignette_uniform_buffer_offset = if let Some(vignette) = maybe_vignette {
430 post_processing_uniform_buffers
431 .vignette
432 .push(&VignetteUniform {
433 intensity: vignette.intensity.min(1.0),
434 radius: vignette.radius.max(1e-6),
435 smoothness: vignette.smoothness.max(0.0),
436 roundness: vignette.roundness.clamp(1e-6, 2.0 - 1e-6),
437 center: vignette.center,
438 edge_compensation: vignette.edge_compensation,
439 unused: 0,
440 color: vignette.color.to_srgba().to_vec4(),
441 })
442 } else {
443 post_processing_uniform_buffers
444 .vignette
445 .push(&VignetteUniform::default())
446 };
447
448 let lens_distortion_uniform_buffer_offset =
449 if let Some(lens_distortion) = maybe_lens_distortion {
450 post_processing_uniform_buffers
451 .lens_distortion
452 .push(&LensDistortionUniform {
453 intensity: lens_distortion.intensity,
454 scale: lens_distortion.scale.max(1e-6),
455 multiplier: lens_distortion.multiplier,
456 center: lens_distortion.center,
457 edge_curvature: lens_distortion.edge_curvature,
458 unused: 0,
459 })
460 } else {
461 post_processing_uniform_buffers
462 .lens_distortion
463 .push(&LensDistortionUniform::default())
464 };
465
466 commands
467 .entity(view_entity)
468 .insert(PostProcessingUniformBufferOffsets {
469 chromatic_aberration: chromatic_aberration_uniform_buffer_offset,
470 vignette: vignette_uniform_buffer_offset,
471 lens_distortion: lens_distortion_uniform_buffer_offset,
472 });
473 }
474
475 post_processing_uniform_buffers
477 .chromatic_aberration
478 .write_buffer(&render_device, &render_queue);
479 post_processing_uniform_buffers
480 .vignette
481 .write_buffer(&render_device, &render_queue);
482 post_processing_uniform_buffers
483 .lens_distortion
484 .write_buffer(&render_device, &render_queue);
485}