Skip to content

Commit 09f1bd0

Browse files
ElabajabaJMS55
andauthored
Add port of AMD's Robust Contrast Adaptive Sharpening (#7422)
# Objective TAA, FXAA, and some other post processing effects can cause the image to become blurry. Sharpening helps to counteract that. ## Solution ~~This is a port of AMD's Contrast Adaptive Sharpening (I ported it from the [SweetFX](https://github.com/CeeJayDK/SweetFX/blob/master/Shaders/CAS.fx) version, which is still MIT licensed). CAS is a good sharpening algorithm that is better at avoiding the full screen oversharpening artifacts that simpler algorithms tend to create.~~ This is a port of AMD's Robust Contrast Adaptive Sharpening (RCAS) which they developed for FSR 1 ([and continue to use in FSR 2](https://github.com/GPUOpen-Effects/FidelityFX-FSR2/blob/149cf26e1229eaf5fecfb4428e71666cf4aee374/src/ffx-fsr2-api/shaders/ffx_fsr1.h#L599)). RCAS is a good sharpening algorithm that is better at avoiding the full screen oversharpening artifacts that simpler algorithms tend to create. --- ## Future Work - Consider porting this to a compute shader for potentially better performance. (In my testing it is currently ridiculously cheap (0.01ms in Bistro at 1440p where I'm GPU bound), so this wasn't a priority, especially since it would increase complexity due to still needing the non-compute version for webgl2 support). --- ## Changelog - Added Contrast Adaptive Sharpening. --------- Co-authored-by: JMS55 <47158642+JMS55@users.noreply.github.com>
1 parent f0f5d79 commit 09f1bd0

File tree

7 files changed

+567
-10
lines changed

7 files changed

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

0 commit comments

Comments
 (0)