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, 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)]
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
57#[deprecated(since = "0.15.0", note = "Renamed to `ContrastAdaptiveSharpening`")]
58pub type ContrastAdaptiveSharpeningSettings = ContrastAdaptiveSharpening;
59
60impl Default for ContrastAdaptiveSharpening {
61    fn default() -> Self {
62        ContrastAdaptiveSharpening {
63            enabled: true,
64            sharpening_strength: 0.6,
65            denoise: false,
66        }
67    }
68}
69
70#[derive(Component, Default, Reflect, Clone)]
71#[reflect(Component, Default)]
72pub struct DenoiseCas(bool);
73
74/// The uniform struct extracted from [`ContrastAdaptiveSharpening`] attached to a [`Camera`].
75/// Will be available for use in the CAS shader.
76#[doc(hidden)]
77#[derive(Component, ShaderType, Clone)]
78pub struct CasUniform {
79    sharpness: f32,
80}
81
82impl ExtractComponent for ContrastAdaptiveSharpening {
83    type QueryData = &'static Self;
84    type QueryFilter = With<Camera>;
85    type Out = (DenoiseCas, CasUniform);
86
87    fn extract_component(item: QueryItem<Self::QueryData>) -> Option<Self::Out> {
88        if !item.enabled || item.sharpening_strength == 0.0 {
89            return None;
90        }
91        Some((
92            DenoiseCas(item.denoise),
93            CasUniform {
94                // above 1.0 causes extreme artifacts and fireflies
95                sharpness: item.sharpening_strength.clamp(0.0, 1.0),
96            },
97        ))
98    }
99}
100
101const CONTRAST_ADAPTIVE_SHARPENING_SHADER_HANDLE: Handle<Shader> =
102    Handle::weak_from_u128(6925381244141981602);
103
104/// Adds Support for Contrast Adaptive Sharpening (CAS).
105pub struct CasPlugin;
106
107impl Plugin for CasPlugin {
108    fn build(&self, app: &mut App) {
109        load_internal_asset!(
110            app,
111            CONTRAST_ADAPTIVE_SHARPENING_SHADER_HANDLE,
112            "robust_contrast_adaptive_sharpening.wgsl",
113            Shader::from_wgsl
114        );
115
116        app.register_type::<ContrastAdaptiveSharpening>();
117        app.add_plugins((
118            ExtractComponentPlugin::<ContrastAdaptiveSharpening>::default(),
119            UniformComponentPlugin::<CasUniform>::default(),
120        ));
121
122        let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
123            return;
124        };
125        render_app
126            .init_resource::<SpecializedRenderPipelines<CasPipeline>>()
127            .add_systems(Render, prepare_cas_pipelines.in_set(RenderSet::Prepare));
128
129        {
130            render_app
131                .add_render_graph_node::<CasNode>(Core3d, Node3d::ContrastAdaptiveSharpening)
132                .add_render_graph_edge(
133                    Core3d,
134                    Node3d::Tonemapping,
135                    Node3d::ContrastAdaptiveSharpening,
136                )
137                .add_render_graph_edges(
138                    Core3d,
139                    (
140                        Node3d::Fxaa,
141                        Node3d::ContrastAdaptiveSharpening,
142                        Node3d::EndMainPassPostProcessing,
143                    ),
144                );
145        }
146        {
147            render_app
148                .add_render_graph_node::<CasNode>(Core2d, Node2d::ContrastAdaptiveSharpening)
149                .add_render_graph_edge(
150                    Core2d,
151                    Node2d::Tonemapping,
152                    Node2d::ContrastAdaptiveSharpening,
153                )
154                .add_render_graph_edges(
155                    Core2d,
156                    (
157                        Node2d::Fxaa,
158                        Node2d::ContrastAdaptiveSharpening,
159                        Node2d::EndMainPassPostProcessing,
160                    ),
161                );
162        }
163    }
164
165    fn finish(&self, app: &mut App) {
166        let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
167            return;
168        };
169        render_app.init_resource::<CasPipeline>();
170    }
171}
172
173#[derive(Resource)]
174pub struct CasPipeline {
175    texture_bind_group: BindGroupLayout,
176    sampler: Sampler,
177}
178
179impl FromWorld for CasPipeline {
180    fn from_world(render_world: &mut World) -> Self {
181        let render_device = render_world.resource::<RenderDevice>();
182        let texture_bind_group = render_device.create_bind_group_layout(
183            "sharpening_texture_bind_group_layout",
184            &BindGroupLayoutEntries::sequential(
185                ShaderStages::FRAGMENT,
186                (
187                    texture_2d(TextureSampleType::Float { filterable: true }),
188                    sampler(SamplerBindingType::Filtering),
189                    // CAS Settings
190                    uniform_buffer::<CasUniform>(true),
191                ),
192            ),
193        );
194
195        let sampler = render_device.create_sampler(&SamplerDescriptor::default());
196
197        CasPipeline {
198            texture_bind_group,
199            sampler,
200        }
201    }
202}
203
204#[derive(PartialEq, Eq, Hash, Clone, Copy)]
205pub struct CasPipelineKey {
206    texture_format: TextureFormat,
207    denoise: bool,
208}
209
210impl SpecializedRenderPipeline for CasPipeline {
211    type Key = CasPipelineKey;
212
213    fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
214        let mut shader_defs = vec![];
215        if key.denoise {
216            shader_defs.push("RCAS_DENOISE".into());
217        }
218        RenderPipelineDescriptor {
219            label: Some("contrast_adaptive_sharpening".into()),
220            layout: vec![self.texture_bind_group.clone()],
221            vertex: fullscreen_shader_vertex_state(),
222            fragment: Some(FragmentState {
223                shader: CONTRAST_ADAPTIVE_SHARPENING_SHADER_HANDLE,
224                shader_defs,
225                entry_point: "fragment".into(),
226                targets: vec![Some(ColorTargetState {
227                    format: key.texture_format,
228                    blend: None,
229                    write_mask: ColorWrites::ALL,
230                })],
231            }),
232            primitive: PrimitiveState::default(),
233            depth_stencil: None,
234            multisample: MultisampleState::default(),
235            push_constant_ranges: Vec::new(),
236            zero_initialize_workgroup_memory: false,
237        }
238    }
239}
240
241fn prepare_cas_pipelines(
242    mut commands: Commands,
243    pipeline_cache: Res<PipelineCache>,
244    mut pipelines: ResMut<SpecializedRenderPipelines<CasPipeline>>,
245    sharpening_pipeline: Res<CasPipeline>,
246    views: Query<
247        (Entity, &ExtractedView, &DenoiseCas),
248        Or<(Added<CasUniform>, Changed<DenoiseCas>)>,
249    >,
250    mut removals: RemovedComponents<CasUniform>,
251) {
252    for entity in removals.read() {
253        commands.entity(entity).remove::<ViewCasPipeline>();
254    }
255
256    for (entity, view, denoise_cas) in &views {
257        let pipeline_id = pipelines.specialize(
258            &pipeline_cache,
259            &sharpening_pipeline,
260            CasPipelineKey {
261                denoise: denoise_cas.0,
262                texture_format: if view.hdr {
263                    ViewTarget::TEXTURE_FORMAT_HDR
264                } else {
265                    TextureFormat::bevy_default()
266                },
267            },
268        );
269
270        commands.entity(entity).insert(ViewCasPipeline(pipeline_id));
271    }
272}
273
274#[derive(Component)]
275pub struct ViewCasPipeline(CachedRenderPipelineId);