1use bevy_app::{App, Plugin};
6use bevy_asset::{
7 embedded_asset, load_embedded_asset, AssetServer, Assets, Handle, RenderAssetUsages,
8};
9use bevy_camera::Camera;
10use bevy_derive::{Deref, DerefMut};
11use bevy_ecs::{
12 component::Component,
13 entity::Entity,
14 query::{QueryItem, With},
15 reflect::ReflectComponent,
16 resource::Resource,
17 schedule::IntoScheduleConfigs as _,
18 system::{lifetimeless::Read, Commands, Query, Res, ResMut},
19 world::World,
20};
21use bevy_image::{BevyDefault, Image};
22use bevy_reflect::{std_traits::ReflectDefault, Reflect};
23use bevy_render::{
24 diagnostic::RecordDiagnostics,
25 extract_component::{ExtractComponent, ExtractComponentPlugin},
26 render_asset::RenderAssets,
27 render_graph::{
28 NodeRunError, RenderGraphContext, RenderGraphExt as _, ViewNode, ViewNodeRunner,
29 },
30 render_resource::{
31 binding_types::{sampler, texture_2d, uniform_buffer},
32 BindGroupEntries, BindGroupLayout, BindGroupLayoutEntries, CachedRenderPipelineId,
33 ColorTargetState, ColorWrites, DynamicUniformBuffer, Extent3d, FilterMode, FragmentState,
34 Operations, PipelineCache, RenderPassColorAttachment, RenderPassDescriptor,
35 RenderPipelineDescriptor, Sampler, SamplerBindingType, SamplerDescriptor, ShaderStages,
36 ShaderType, SpecializedRenderPipeline, SpecializedRenderPipelines, TextureDimension,
37 TextureFormat, TextureSampleType,
38 },
39 renderer::{RenderContext, RenderDevice, RenderQueue},
40 texture::GpuImage,
41 view::{ExtractedView, ViewTarget},
42 Render, RenderApp, RenderStartup, RenderSystems,
43};
44use bevy_shader::{load_shader_library, Shader};
45use bevy_utils::prelude::default;
46
47use bevy_core_pipeline::{
48 core_2d::graph::{Core2d, Node2d},
49 core_3d::graph::{Core3d, Node3d},
50 FullscreenShader,
51};
52
53const DEFAULT_CHROMATIC_ABERRATION_INTENSITY: f32 = 0.02;
56
57const DEFAULT_CHROMATIC_ABERRATION_MAX_SAMPLES: u32 = 8;
59
60static DEFAULT_CHROMATIC_ABERRATION_LUT_DATA: [u8; 12] =
65 [255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255];
66
67#[derive(Resource)]
68struct DefaultChromaticAberrationLut(Handle<Image>);
69
70#[derive(Default)]
75pub struct EffectStackPlugin;
76
77#[derive(Reflect, Component, Clone)]
95#[reflect(Component, Default, Clone)]
96pub struct ChromaticAberration {
97 pub color_lut: Option<Handle<Image>>,
107
108 pub intensity: f32,
113
114 pub max_samples: u32,
120}
121
122#[derive(Resource)]
126pub struct PostProcessingPipeline {
127 bind_group_layout: BindGroupLayout,
129 source_sampler: Sampler,
131 chromatic_aberration_lut_sampler: Sampler,
133 fullscreen_shader: FullscreenShader,
135 fragment_shader: Handle<Shader>,
137}
138
139#[derive(Clone, Copy, PartialEq, Eq, Hash)]
141pub struct PostProcessingPipelineKey {
142 texture_format: TextureFormat,
144}
145
146#[derive(Component, Deref, DerefMut)]
149pub struct PostProcessingPipelineId(CachedRenderPipelineId);
150
151#[derive(ShaderType)]
156pub struct ChromaticAberrationUniform {
157 intensity: f32,
159 max_samples: u32,
162 unused_1: u32,
164 unused_2: u32,
166}
167
168#[derive(Resource, Deref, DerefMut, Default)]
171pub struct PostProcessingUniformBuffers {
172 chromatic_aberration: DynamicUniformBuffer<ChromaticAberrationUniform>,
173}
174
175#[derive(Component, Deref, DerefMut)]
179pub struct PostProcessingUniformBufferOffsets {
180 chromatic_aberration: u32,
181}
182
183#[derive(Default)]
185pub struct PostProcessingNode;
186
187impl Plugin for EffectStackPlugin {
188 fn build(&self, app: &mut App) {
189 load_shader_library!(app, "chromatic_aberration.wgsl");
190
191 embedded_asset!(app, "post_process.wgsl");
192
193 let mut assets = app.world_mut().resource_mut::<Assets<_>>();
195 let default_lut = assets.add(Image::new(
196 Extent3d {
197 width: 3,
198 height: 1,
199 depth_or_array_layers: 1,
200 },
201 TextureDimension::D2,
202 DEFAULT_CHROMATIC_ABERRATION_LUT_DATA.to_vec(),
203 TextureFormat::Rgba8UnormSrgb,
204 RenderAssetUsages::RENDER_WORLD,
205 ));
206
207 app.add_plugins(ExtractComponentPlugin::<ChromaticAberration>::default());
208
209 let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
210 return;
211 };
212
213 render_app
214 .insert_resource(DefaultChromaticAberrationLut(default_lut))
215 .init_resource::<SpecializedRenderPipelines<PostProcessingPipeline>>()
216 .init_resource::<PostProcessingUniformBuffers>()
217 .add_systems(RenderStartup, init_post_processing_pipeline)
218 .add_systems(
219 Render,
220 (
221 prepare_post_processing_pipelines,
222 prepare_post_processing_uniforms,
223 )
224 .in_set(RenderSystems::Prepare),
225 )
226 .add_render_graph_node::<ViewNodeRunner<PostProcessingNode>>(
227 Core3d,
228 Node3d::PostProcessing,
229 )
230 .add_render_graph_edges(
231 Core3d,
232 (
233 Node3d::DepthOfField,
234 Node3d::PostProcessing,
235 Node3d::Tonemapping,
236 ),
237 )
238 .add_render_graph_node::<ViewNodeRunner<PostProcessingNode>>(
239 Core2d,
240 Node2d::PostProcessing,
241 )
242 .add_render_graph_edges(
243 Core2d,
244 (Node2d::Bloom, Node2d::PostProcessing, Node2d::Tonemapping),
245 );
246 }
247}
248
249impl Default for ChromaticAberration {
250 fn default() -> Self {
251 Self {
252 color_lut: None,
253 intensity: DEFAULT_CHROMATIC_ABERRATION_INTENSITY,
254 max_samples: DEFAULT_CHROMATIC_ABERRATION_MAX_SAMPLES,
255 }
256 }
257}
258
259pub fn init_post_processing_pipeline(
260 mut commands: Commands,
261 render_device: Res<RenderDevice>,
262 fullscreen_shader: Res<FullscreenShader>,
263 asset_server: Res<AssetServer>,
264) {
265 let bind_group_layout = render_device.create_bind_group_layout(
267 Some("postprocessing bind group layout"),
268 &BindGroupLayoutEntries::sequential(
269 ShaderStages::FRAGMENT,
270 (
271 texture_2d(TextureSampleType::Float { filterable: true }),
273 sampler(SamplerBindingType::Filtering),
275 texture_2d(TextureSampleType::Float { filterable: true }),
277 sampler(SamplerBindingType::Filtering),
279 uniform_buffer::<ChromaticAberrationUniform>(true),
281 ),
282 ),
283 );
284
285 let source_sampler = render_device.create_sampler(&SamplerDescriptor {
289 mipmap_filter: FilterMode::Linear,
290 min_filter: FilterMode::Linear,
291 mag_filter: FilterMode::Linear,
292 ..default()
293 });
294
295 let chromatic_aberration_lut_sampler = render_device.create_sampler(&SamplerDescriptor {
296 mipmap_filter: FilterMode::Linear,
297 min_filter: FilterMode::Linear,
298 mag_filter: FilterMode::Linear,
299 ..default()
300 });
301
302 commands.insert_resource(PostProcessingPipeline {
303 bind_group_layout,
304 source_sampler,
305 chromatic_aberration_lut_sampler,
306 fullscreen_shader: fullscreen_shader.clone(),
307 fragment_shader: load_embedded_asset!(asset_server.as_ref(), "post_process.wgsl"),
308 });
309}
310
311impl SpecializedRenderPipeline for PostProcessingPipeline {
312 type Key = PostProcessingPipelineKey;
313
314 fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
315 RenderPipelineDescriptor {
316 label: Some("postprocessing".into()),
317 layout: vec![self.bind_group_layout.clone()],
318 vertex: self.fullscreen_shader.to_vertex_state(),
319 fragment: Some(FragmentState {
320 shader: self.fragment_shader.clone(),
321 targets: vec![Some(ColorTargetState {
322 format: key.texture_format,
323 blend: None,
324 write_mask: ColorWrites::ALL,
325 })],
326 ..default()
327 }),
328 ..default()
329 }
330 }
331}
332
333impl ViewNode for PostProcessingNode {
334 type ViewQuery = (
335 Read<ViewTarget>,
336 Read<PostProcessingPipelineId>,
337 Read<ChromaticAberration>,
338 Read<PostProcessingUniformBufferOffsets>,
339 );
340
341 fn run<'w>(
342 &self,
343 _: &mut RenderGraphContext,
344 render_context: &mut RenderContext<'w>,
345 (view_target, pipeline_id, chromatic_aberration, post_processing_uniform_buffer_offsets): QueryItem<'w, '_, Self::ViewQuery>,
346 world: &'w World,
347 ) -> Result<(), NodeRunError> {
348 let pipeline_cache = world.resource::<PipelineCache>();
349 let post_processing_pipeline = world.resource::<PostProcessingPipeline>();
350 let post_processing_uniform_buffers = world.resource::<PostProcessingUniformBuffers>();
351 let gpu_image_assets = world.resource::<RenderAssets<GpuImage>>();
352 let default_lut = world.resource::<DefaultChromaticAberrationLut>();
353
354 let Some(pipeline) = pipeline_cache.get_render_pipeline(**pipeline_id) else {
356 return Ok(());
357 };
358
359 let Some(chromatic_aberration_lut) = gpu_image_assets.get(
361 chromatic_aberration
362 .color_lut
363 .as_ref()
364 .unwrap_or(&default_lut.0),
365 ) else {
366 return Ok(());
367 };
368
369 let Some(chromatic_aberration_uniform_buffer_binding) = post_processing_uniform_buffers
371 .chromatic_aberration
372 .binding()
373 else {
374 return Ok(());
375 };
376
377 let diagnostics = render_context.diagnostic_recorder();
378
379 let post_process = view_target.post_process_write();
382
383 let pass_descriptor = RenderPassDescriptor {
384 label: Some("postprocessing"),
385 color_attachments: &[Some(RenderPassColorAttachment {
386 view: post_process.destination,
387 depth_slice: None,
388 resolve_target: None,
389 ops: Operations::default(),
390 })],
391 depth_stencil_attachment: None,
392 timestamp_writes: None,
393 occlusion_query_set: None,
394 };
395
396 let bind_group = render_context.render_device().create_bind_group(
397 Some("postprocessing bind group"),
398 &post_processing_pipeline.bind_group_layout,
399 &BindGroupEntries::sequential((
400 post_process.source,
401 &post_processing_pipeline.source_sampler,
402 &chromatic_aberration_lut.texture_view,
403 &post_processing_pipeline.chromatic_aberration_lut_sampler,
404 chromatic_aberration_uniform_buffer_binding,
405 )),
406 );
407
408 let mut render_pass = render_context
409 .command_encoder()
410 .begin_render_pass(&pass_descriptor);
411 let pass_span = diagnostics.pass_span(&mut render_pass, "postprocessing");
412
413 render_pass.set_pipeline(pipeline);
414 render_pass.set_bind_group(0, &bind_group, &[**post_processing_uniform_buffer_offsets]);
415 render_pass.draw(0..3, 0..1);
416
417 pass_span.end(&mut render_pass);
418
419 Ok(())
420 }
421}
422
423pub fn prepare_post_processing_pipelines(
425 mut commands: Commands,
426 pipeline_cache: Res<PipelineCache>,
427 mut pipelines: ResMut<SpecializedRenderPipelines<PostProcessingPipeline>>,
428 post_processing_pipeline: Res<PostProcessingPipeline>,
429 views: Query<(Entity, &ExtractedView), With<ChromaticAberration>>,
430) {
431 for (entity, view) in views.iter() {
432 let pipeline_id = pipelines.specialize(
433 &pipeline_cache,
434 &post_processing_pipeline,
435 PostProcessingPipelineKey {
436 texture_format: if view.hdr {
437 ViewTarget::TEXTURE_FORMAT_HDR
438 } else {
439 TextureFormat::bevy_default()
440 },
441 },
442 );
443
444 commands
445 .entity(entity)
446 .insert(PostProcessingPipelineId(pipeline_id));
447 }
448}
449
450pub fn prepare_post_processing_uniforms(
453 mut commands: Commands,
454 mut post_processing_uniform_buffers: ResMut<PostProcessingUniformBuffers>,
455 render_device: Res<RenderDevice>,
456 render_queue: Res<RenderQueue>,
457 mut views: Query<(Entity, &ChromaticAberration)>,
458) {
459 post_processing_uniform_buffers.clear();
460
461 for (view_entity, chromatic_aberration) in views.iter_mut() {
463 let chromatic_aberration_uniform_buffer_offset =
464 post_processing_uniform_buffers.push(&ChromaticAberrationUniform {
465 intensity: chromatic_aberration.intensity,
466 max_samples: chromatic_aberration.max_samples,
467 unused_1: 0,
468 unused_2: 0,
469 });
470 commands
471 .entity(view_entity)
472 .insert(PostProcessingUniformBufferOffsets {
473 chromatic_aberration: chromatic_aberration_uniform_buffer_offset,
474 });
475 }
476
477 post_processing_uniform_buffers.write_buffer(&render_device, &render_queue);
479}
480
481impl ExtractComponent for ChromaticAberration {
482 type QueryData = Read<ChromaticAberration>;
483
484 type QueryFilter = With<Camera>;
485
486 type Out = ChromaticAberration;
487
488 fn extract_component(
489 chromatic_aberration: QueryItem<'_, '_, Self::QueryData>,
490 ) -> Option<Self::Out> {
491 if chromatic_aberration.intensity > 0.0 {
493 Some(chromatic_aberration.clone())
494 } else {
495 None
496 }
497 }
498}