|
| 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