1use bevy_app::{App, Plugin};
6use bevy_asset::{load_internal_asset, weak_handle, Assets, Handle};
7use bevy_derive::{Deref, DerefMut};
8use bevy_ecs::{
9 component::Component,
10 entity::Entity,
11 query::{QueryItem, With},
12 reflect::ReflectComponent,
13 resource::Resource,
14 schedule::IntoScheduleConfigs as _,
15 system::{lifetimeless::Read, Commands, Query, Res, ResMut},
16 world::{FromWorld, World},
17};
18use bevy_image::{BevyDefault, Image};
19use bevy_reflect::{std_traits::ReflectDefault, Reflect};
20use bevy_render::{
21 camera::Camera,
22 extract_component::{ExtractComponent, ExtractComponentPlugin},
23 render_asset::{RenderAssetUsages, RenderAssets},
24 render_graph::{
25 NodeRunError, RenderGraphApp as _, RenderGraphContext, ViewNode, ViewNodeRunner,
26 },
27 render_resource::{
28 binding_types::{sampler, texture_2d, uniform_buffer},
29 BindGroupEntries, BindGroupLayout, BindGroupLayoutEntries, CachedRenderPipelineId,
30 ColorTargetState, ColorWrites, DynamicUniformBuffer, Extent3d, FilterMode, FragmentState,
31 Operations, PipelineCache, RenderPassColorAttachment, RenderPassDescriptor,
32 RenderPipelineDescriptor, Sampler, SamplerBindingType, SamplerDescriptor, Shader,
33 ShaderStages, ShaderType, SpecializedRenderPipeline, SpecializedRenderPipelines,
34 TextureDimension, TextureFormat, TextureSampleType,
35 },
36 renderer::{RenderContext, RenderDevice, RenderQueue},
37 texture::GpuImage,
38 view::{ExtractedView, ViewTarget},
39 Render, RenderApp, RenderSet,
40};
41use bevy_utils::prelude::default;
42
43use crate::{
44 core_2d::graph::{Core2d, Node2d},
45 core_3d::graph::{Core3d, Node3d},
46 fullscreen_vertex_shader,
47};
48
49const POST_PROCESSING_SHADER_HANDLE: Handle<Shader> =
51 weak_handle!("5e8e627a-7531-484d-a988-9a38acb34e52");
52const CHROMATIC_ABERRATION_SHADER_HANDLE: Handle<Shader> =
54 weak_handle!("e598550e-71c3-4f5a-ba29-aebc3f88c7b5");
55
56const DEFAULT_CHROMATIC_ABERRATION_LUT_HANDLE: Handle<Image> =
61 weak_handle!("dc3e3307-40a1-49bb-be6d-e0634e8836b2");
62
63const DEFAULT_CHROMATIC_ABERRATION_INTENSITY: f32 = 0.02;
66
67const DEFAULT_CHROMATIC_ABERRATION_MAX_SAMPLES: u32 = 8;
69
70static DEFAULT_CHROMATIC_ABERRATION_LUT_DATA: [u8; 12] =
75 [255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255];
76
77pub struct PostProcessingPlugin;
82
83#[derive(Reflect, Component, Clone)]
101#[reflect(Component, Default, Clone)]
102pub struct ChromaticAberration {
103 pub color_lut: Handle<Image>,
113
114 pub intensity: f32,
119
120 pub max_samples: u32,
126}
127
128#[derive(Resource)]
132pub struct PostProcessingPipeline {
133 bind_group_layout: BindGroupLayout,
135 source_sampler: Sampler,
137 chromatic_aberration_lut_sampler: Sampler,
139}
140
141#[derive(Clone, Copy, PartialEq, Eq, Hash)]
143pub struct PostProcessingPipelineKey {
144 texture_format: TextureFormat,
146}
147
148#[derive(Component, Deref, DerefMut)]
151pub struct PostProcessingPipelineId(CachedRenderPipelineId);
152
153#[derive(ShaderType)]
158pub struct ChromaticAberrationUniform {
159 intensity: f32,
161 max_samples: u32,
164 unused_1: u32,
166 unused_2: u32,
168}
169
170#[derive(Resource, Deref, DerefMut, Default)]
173pub struct PostProcessingUniformBuffers {
174 chromatic_aberration: DynamicUniformBuffer<ChromaticAberrationUniform>,
175}
176
177#[derive(Component, Deref, DerefMut)]
181pub struct PostProcessingUniformBufferOffsets {
182 chromatic_aberration: u32,
183}
184
185#[derive(Default)]
187pub struct PostProcessingNode;
188
189impl Plugin for PostProcessingPlugin {
190 fn build(&self, app: &mut App) {
191 load_internal_asset!(
192 app,
193 POST_PROCESSING_SHADER_HANDLE,
194 "post_process.wgsl",
195 Shader::from_wgsl
196 );
197 load_internal_asset!(
198 app,
199 CHROMATIC_ABERRATION_SHADER_HANDLE,
200 "chromatic_aberration.wgsl",
201 Shader::from_wgsl
202 );
203
204 let mut assets = app.world_mut().resource_mut::<Assets<_>>();
206 assets.insert(
207 DEFAULT_CHROMATIC_ABERRATION_LUT_HANDLE.id(),
208 Image::new(
209 Extent3d {
210 width: 3,
211 height: 1,
212 depth_or_array_layers: 1,
213 },
214 TextureDimension::D2,
215 DEFAULT_CHROMATIC_ABERRATION_LUT_DATA.to_vec(),
216 TextureFormat::Rgba8UnormSrgb,
217 RenderAssetUsages::RENDER_WORLD,
218 ),
219 );
220
221 app.register_type::<ChromaticAberration>();
222 app.add_plugins(ExtractComponentPlugin::<ChromaticAberration>::default());
223
224 let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
225 return;
226 };
227
228 render_app
229 .init_resource::<SpecializedRenderPipelines<PostProcessingPipeline>>()
230 .init_resource::<PostProcessingUniformBuffers>()
231 .add_systems(
232 Render,
233 (
234 prepare_post_processing_pipelines,
235 prepare_post_processing_uniforms,
236 )
237 .in_set(RenderSet::Prepare),
238 )
239 .add_render_graph_node::<ViewNodeRunner<PostProcessingNode>>(
240 Core3d,
241 Node3d::PostProcessing,
242 )
243 .add_render_graph_edges(
244 Core3d,
245 (
246 Node3d::DepthOfField,
247 Node3d::PostProcessing,
248 Node3d::Tonemapping,
249 ),
250 )
251 .add_render_graph_node::<ViewNodeRunner<PostProcessingNode>>(
252 Core2d,
253 Node2d::PostProcessing,
254 )
255 .add_render_graph_edges(
256 Core2d,
257 (Node2d::Bloom, Node2d::PostProcessing, Node2d::Tonemapping),
258 );
259 }
260
261 fn finish(&self, app: &mut App) {
262 let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
263 return;
264 };
265 render_app.init_resource::<PostProcessingPipeline>();
266 }
267}
268
269impl Default for ChromaticAberration {
270 fn default() -> Self {
271 Self {
272 color_lut: DEFAULT_CHROMATIC_ABERRATION_LUT_HANDLE,
273 intensity: DEFAULT_CHROMATIC_ABERRATION_INTENSITY,
274 max_samples: DEFAULT_CHROMATIC_ABERRATION_MAX_SAMPLES,
275 }
276 }
277}
278
279impl FromWorld for PostProcessingPipeline {
280 fn from_world(world: &mut World) -> Self {
281 let render_device = world.resource::<RenderDevice>();
282
283 let bind_group_layout = render_device.create_bind_group_layout(
285 Some("postprocessing bind group layout"),
286 &BindGroupLayoutEntries::sequential(
287 ShaderStages::FRAGMENT,
288 (
289 texture_2d(TextureSampleType::Float { filterable: true }),
291 sampler(SamplerBindingType::Filtering),
293 texture_2d(TextureSampleType::Float { filterable: true }),
295 sampler(SamplerBindingType::Filtering),
297 uniform_buffer::<ChromaticAberrationUniform>(true),
299 ),
300 ),
301 );
302
303 let source_sampler = render_device.create_sampler(&SamplerDescriptor {
307 mipmap_filter: FilterMode::Linear,
308 min_filter: FilterMode::Linear,
309 mag_filter: FilterMode::Linear,
310 ..default()
311 });
312
313 let chromatic_aberration_lut_sampler = render_device.create_sampler(&SamplerDescriptor {
314 mipmap_filter: FilterMode::Linear,
315 min_filter: FilterMode::Linear,
316 mag_filter: FilterMode::Linear,
317 ..default()
318 });
319
320 PostProcessingPipeline {
321 bind_group_layout,
322 source_sampler,
323 chromatic_aberration_lut_sampler,
324 }
325 }
326}
327
328impl SpecializedRenderPipeline for PostProcessingPipeline {
329 type Key = PostProcessingPipelineKey;
330
331 fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
332 RenderPipelineDescriptor {
333 label: Some("postprocessing".into()),
334 layout: vec![self.bind_group_layout.clone()],
335 vertex: fullscreen_vertex_shader::fullscreen_shader_vertex_state(),
336 fragment: Some(FragmentState {
337 shader: POST_PROCESSING_SHADER_HANDLE,
338 shader_defs: vec![],
339 entry_point: "fragment_main".into(),
340 targets: vec![Some(ColorTargetState {
341 format: key.texture_format,
342 blend: None,
343 write_mask: ColorWrites::ALL,
344 })],
345 }),
346 primitive: default(),
347 depth_stencil: None,
348 multisample: default(),
349 push_constant_ranges: vec![],
350 zero_initialize_workgroup_memory: false,
351 }
352 }
353}
354
355impl ViewNode for PostProcessingNode {
356 type ViewQuery = (
357 Read<ViewTarget>,
358 Read<PostProcessingPipelineId>,
359 Read<ChromaticAberration>,
360 Read<PostProcessingUniformBufferOffsets>,
361 );
362
363 fn run<'w>(
364 &self,
365 _: &mut RenderGraphContext,
366 render_context: &mut RenderContext<'w>,
367 (view_target, pipeline_id, chromatic_aberration, post_processing_uniform_buffer_offsets): QueryItem<'w, Self::ViewQuery>,
368 world: &'w World,
369 ) -> Result<(), NodeRunError> {
370 let pipeline_cache = world.resource::<PipelineCache>();
371 let post_processing_pipeline = world.resource::<PostProcessingPipeline>();
372 let post_processing_uniform_buffers = world.resource::<PostProcessingUniformBuffers>();
373 let gpu_image_assets = world.resource::<RenderAssets<GpuImage>>();
374
375 let Some(pipeline) = pipeline_cache.get_render_pipeline(**pipeline_id) else {
377 return Ok(());
378 };
379
380 let Some(chromatic_aberration_lut) = gpu_image_assets.get(&chromatic_aberration.color_lut)
382 else {
383 return Ok(());
384 };
385
386 let Some(chromatic_aberration_uniform_buffer_binding) = post_processing_uniform_buffers
388 .chromatic_aberration
389 .binding()
390 else {
391 return Ok(());
392 };
393
394 let post_process = view_target.post_process_write();
397
398 let pass_descriptor = RenderPassDescriptor {
399 label: Some("postprocessing pass"),
400 color_attachments: &[Some(RenderPassColorAttachment {
401 view: post_process.destination,
402 resolve_target: None,
403 ops: Operations::default(),
404 })],
405 depth_stencil_attachment: None,
406 timestamp_writes: None,
407 occlusion_query_set: None,
408 };
409
410 let bind_group = render_context.render_device().create_bind_group(
411 Some("postprocessing bind group"),
412 &post_processing_pipeline.bind_group_layout,
413 &BindGroupEntries::sequential((
414 post_process.source,
415 &post_processing_pipeline.source_sampler,
416 &chromatic_aberration_lut.texture_view,
417 &post_processing_pipeline.chromatic_aberration_lut_sampler,
418 chromatic_aberration_uniform_buffer_binding,
419 )),
420 );
421
422 let mut render_pass = render_context
423 .command_encoder()
424 .begin_render_pass(&pass_descriptor);
425
426 render_pass.set_pipeline(pipeline);
427 render_pass.set_bind_group(0, &bind_group, &[**post_processing_uniform_buffer_offsets]);
428 render_pass.draw(0..3, 0..1);
429
430 Ok(())
431 }
432}
433
434pub fn prepare_post_processing_pipelines(
436 mut commands: Commands,
437 pipeline_cache: Res<PipelineCache>,
438 mut pipelines: ResMut<SpecializedRenderPipelines<PostProcessingPipeline>>,
439 post_processing_pipeline: Res<PostProcessingPipeline>,
440 views: Query<(Entity, &ExtractedView), With<ChromaticAberration>>,
441) {
442 for (entity, view) in views.iter() {
443 let pipeline_id = pipelines.specialize(
444 &pipeline_cache,
445 &post_processing_pipeline,
446 PostProcessingPipelineKey {
447 texture_format: if view.hdr {
448 ViewTarget::TEXTURE_FORMAT_HDR
449 } else {
450 TextureFormat::bevy_default()
451 },
452 },
453 );
454
455 commands
456 .entity(entity)
457 .insert(PostProcessingPipelineId(pipeline_id));
458 }
459}
460
461pub fn prepare_post_processing_uniforms(
464 mut commands: Commands,
465 mut post_processing_uniform_buffers: ResMut<PostProcessingUniformBuffers>,
466 render_device: Res<RenderDevice>,
467 render_queue: Res<RenderQueue>,
468 mut views: Query<(Entity, &ChromaticAberration)>,
469) {
470 post_processing_uniform_buffers.clear();
471
472 for (view_entity, chromatic_aberration) in views.iter_mut() {
474 let chromatic_aberration_uniform_buffer_offset =
475 post_processing_uniform_buffers.push(&ChromaticAberrationUniform {
476 intensity: chromatic_aberration.intensity,
477 max_samples: chromatic_aberration.max_samples,
478 unused_1: 0,
479 unused_2: 0,
480 });
481 commands
482 .entity(view_entity)
483 .insert(PostProcessingUniformBufferOffsets {
484 chromatic_aberration: chromatic_aberration_uniform_buffer_offset,
485 });
486 }
487
488 post_processing_uniform_buffers.write_buffer(&render_device, &render_queue);
490}
491
492impl ExtractComponent for ChromaticAberration {
493 type QueryData = Read<ChromaticAberration>;
494
495 type QueryFilter = With<Camera>;
496
497 type Out = ChromaticAberration;
498
499 fn extract_component(
500 chromatic_aberration: QueryItem<'_, Self::QueryData>,
501 ) -> Option<Self::Out> {
502 if chromatic_aberration.intensity > 0.0 {
504 Some(chromatic_aberration.clone())
505 } else {
506 None
507 }
508 }
509}