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