bevy_core_pipeline/contrast_adaptive_sharpening/
mod.rs

1use crate::{
2    core_2d::graph::{Core2d, Node2d},
3    core_3d::graph::{Core3d, Node3d},
4    fullscreen_vertex_shader::fullscreen_shader_vertex_state,
5};
6use bevy_app::prelude::*;
7use bevy_asset::{load_internal_asset, weak_handle, Handle};
8use bevy_ecs::{prelude::*, query::QueryItem};
9use bevy_image::BevyDefault as _;
10use bevy_reflect::{std_traits::ReflectDefault, Reflect};
11use bevy_render::{
12    extract_component::{ExtractComponent, ExtractComponentPlugin, UniformComponentPlugin},
13    prelude::Camera,
14    render_graph::RenderGraphApp,
15    render_resource::{
16        binding_types::{sampler, texture_2d, uniform_buffer},
17        *,
18    },
19    renderer::RenderDevice,
20    view::{ExtractedView, ViewTarget},
21    Render, RenderApp, RenderSet,
22};
23
24mod node;
25
26pub use node::CasNode;
27
28/// Applies a contrast adaptive sharpening (CAS) filter to the camera.
29///
30/// CAS is usually used in combination with shader based anti-aliasing methods
31/// such as FXAA or TAA to regain some of the lost detail from the blurring that they introduce.
32///
33/// CAS is designed to adjust the amount of sharpening applied to different areas of an image
34/// based on the local contrast. This can help avoid over-sharpening areas with high contrast
35/// and under-sharpening areas with low contrast.
36///
37/// To use this, add the [`ContrastAdaptiveSharpening`] component to a 2D or 3D camera.
38#[derive(Component, Reflect, Clone)]
39#[reflect(Component, Default, Clone)]
40pub struct ContrastAdaptiveSharpening {
41    /// Enable or disable sharpening.
42    pub enabled: bool,
43    /// Adjusts sharpening strength. Higher values increase the amount of sharpening.
44    ///
45    /// Clamped between 0.0 and 1.0.
46    ///
47    /// The default value is 0.6.
48    pub sharpening_strength: f32,
49    /// Whether to try and avoid sharpening areas that are already noisy.
50    ///
51    /// You probably shouldn't use this, and just leave it set to false.
52    /// You should generally apply any sort of film grain or similar effects after CAS
53    /// and upscaling to avoid artifacts.
54    pub denoise: bool,
55}
56
57impl Default for ContrastAdaptiveSharpening {
58    fn default() -> Self {
59        ContrastAdaptiveSharpening {
60            enabled: true,
61            sharpening_strength: 0.6,
62            denoise: false,
63        }
64    }
65}
66
67#[derive(Component, Default, Reflect, Clone)]
68#[reflect(Component, Default, Clone)]
69pub struct DenoiseCas(bool);
70
71/// The uniform struct extracted from [`ContrastAdaptiveSharpening`] attached to a [`Camera`].
72/// Will be available for use in the CAS shader.
73#[doc(hidden)]
74#[derive(Component, ShaderType, Clone)]
75pub struct CasUniform {
76    sharpness: f32,
77}
78
79impl ExtractComponent for ContrastAdaptiveSharpening {
80    type QueryData = &'static Self;
81    type QueryFilter = With<Camera>;
82    type Out = (DenoiseCas, CasUniform);
83
84    fn extract_component(item: QueryItem<Self::QueryData>) -> Option<Self::Out> {
85        if !item.enabled || item.sharpening_strength == 0.0 {
86            return None;
87        }
88        Some((
89            DenoiseCas(item.denoise),
90            CasUniform {
91                // above 1.0 causes extreme artifacts and fireflies
92                sharpness: item.sharpening_strength.clamp(0.0, 1.0),
93            },
94        ))
95    }
96}
97
98const CONTRAST_ADAPTIVE_SHARPENING_SHADER_HANDLE: Handle<Shader> =
99    weak_handle!("ef83f0a5-51df-4b51-9ab7-b5fd1ae5a397");
100
101/// Adds Support for Contrast Adaptive Sharpening (CAS).
102pub struct CasPlugin;
103
104impl Plugin for CasPlugin {
105    fn build(&self, app: &mut App) {
106        load_internal_asset!(
107            app,
108            CONTRAST_ADAPTIVE_SHARPENING_SHADER_HANDLE,
109            "robust_contrast_adaptive_sharpening.wgsl",
110            Shader::from_wgsl
111        );
112
113        app.register_type::<ContrastAdaptiveSharpening>();
114        app.add_plugins((
115            ExtractComponentPlugin::<ContrastAdaptiveSharpening>::default(),
116            UniformComponentPlugin::<CasUniform>::default(),
117        ));
118
119        let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
120            return;
121        };
122        render_app
123            .init_resource::<SpecializedRenderPipelines<CasPipeline>>()
124            .add_systems(Render, prepare_cas_pipelines.in_set(RenderSet::Prepare));
125
126        {
127            render_app
128                .add_render_graph_node::<CasNode>(Core3d, Node3d::ContrastAdaptiveSharpening)
129                .add_render_graph_edge(
130                    Core3d,
131                    Node3d::Tonemapping,
132                    Node3d::ContrastAdaptiveSharpening,
133                )
134                .add_render_graph_edges(
135                    Core3d,
136                    (
137                        Node3d::Fxaa,
138                        Node3d::ContrastAdaptiveSharpening,
139                        Node3d::EndMainPassPostProcessing,
140                    ),
141                );
142        }
143        {
144            render_app
145                .add_render_graph_node::<CasNode>(Core2d, Node2d::ContrastAdaptiveSharpening)
146                .add_render_graph_edge(
147                    Core2d,
148                    Node2d::Tonemapping,
149                    Node2d::ContrastAdaptiveSharpening,
150                )
151                .add_render_graph_edges(
152                    Core2d,
153                    (
154                        Node2d::Fxaa,
155                        Node2d::ContrastAdaptiveSharpening,
156                        Node2d::EndMainPassPostProcessing,
157                    ),
158                );
159        }
160    }
161
162    fn finish(&self, app: &mut App) {
163        let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
164            return;
165        };
166        render_app.init_resource::<CasPipeline>();
167    }
168}
169
170#[derive(Resource)]
171pub struct CasPipeline {
172    texture_bind_group: BindGroupLayout,
173    sampler: Sampler,
174}
175
176impl FromWorld for CasPipeline {
177    fn from_world(render_world: &mut World) -> Self {
178        let render_device = render_world.resource::<RenderDevice>();
179        let texture_bind_group = render_device.create_bind_group_layout(
180            "sharpening_texture_bind_group_layout",
181            &BindGroupLayoutEntries::sequential(
182                ShaderStages::FRAGMENT,
183                (
184                    texture_2d(TextureSampleType::Float { filterable: true }),
185                    sampler(SamplerBindingType::Filtering),
186                    // CAS Settings
187                    uniform_buffer::<CasUniform>(true),
188                ),
189            ),
190        );
191
192        let sampler = render_device.create_sampler(&SamplerDescriptor::default());
193
194        CasPipeline {
195            texture_bind_group,
196            sampler,
197        }
198    }
199}
200
201#[derive(PartialEq, Eq, Hash, Clone, Copy)]
202pub struct CasPipelineKey {
203    texture_format: TextureFormat,
204    denoise: bool,
205}
206
207impl SpecializedRenderPipeline for CasPipeline {
208    type Key = CasPipelineKey;
209
210    fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
211        let mut shader_defs = vec![];
212        if key.denoise {
213            shader_defs.push("RCAS_DENOISE".into());
214        }
215        RenderPipelineDescriptor {
216            label: Some("contrast_adaptive_sharpening".into()),
217            layout: vec![self.texture_bind_group.clone()],
218            vertex: fullscreen_shader_vertex_state(),
219            fragment: Some(FragmentState {
220                shader: CONTRAST_ADAPTIVE_SHARPENING_SHADER_HANDLE,
221                shader_defs,
222                entry_point: "fragment".into(),
223                targets: vec![Some(ColorTargetState {
224                    format: key.texture_format,
225                    blend: None,
226                    write_mask: ColorWrites::ALL,
227                })],
228            }),
229            primitive: PrimitiveState::default(),
230            depth_stencil: None,
231            multisample: MultisampleState::default(),
232            push_constant_ranges: Vec::new(),
233            zero_initialize_workgroup_memory: false,
234        }
235    }
236}
237
238fn prepare_cas_pipelines(
239    mut commands: Commands,
240    pipeline_cache: Res<PipelineCache>,
241    mut pipelines: ResMut<SpecializedRenderPipelines<CasPipeline>>,
242    sharpening_pipeline: Res<CasPipeline>,
243    views: Query<
244        (Entity, &ExtractedView, &DenoiseCas),
245        Or<(Added<CasUniform>, Changed<DenoiseCas>)>,
246    >,
247    mut removals: RemovedComponents<CasUniform>,
248) {
249    for entity in removals.read() {
250        commands.entity(entity).remove::<ViewCasPipeline>();
251    }
252
253    for (entity, view, denoise_cas) in &views {
254        let pipeline_id = pipelines.specialize(
255            &pipeline_cache,
256            &sharpening_pipeline,
257            CasPipelineKey {
258                denoise: denoise_cas.0,
259                texture_format: if view.hdr {
260                    ViewTarget::TEXTURE_FORMAT_HDR
261                } else {
262                    TextureFormat::bevy_default()
263                },
264            },
265        );
266
267        commands.entity(entity).insert(ViewCasPipeline(pipeline_id));
268    }
269}
270
271#[derive(Component)]
272pub struct ViewCasPipeline(CachedRenderPipelineId);