diff --git a/crates/bevy_core_pipeline/src/contrast_adaptive_sharpening/mod.rs b/crates/bevy_core_pipeline/src/contrast_adaptive_sharpening/mod.rs new file mode 100644 index 0000000000..1f189f15aa --- /dev/null +++ b/crates/bevy_core_pipeline/src/contrast_adaptive_sharpening/mod.rs @@ -0,0 +1,275 @@ +use crate::{core_2d, core_3d, fullscreen_vertex_shader::fullscreen_shader_vertex_state}; +use bevy_app::prelude::*; +use bevy_asset::{load_internal_asset, HandleUntyped}; +use bevy_ecs::{prelude::*, query::QueryItem}; +use bevy_reflect::{Reflect, TypeUuid}; +use bevy_render::{ + extract_component::{ExtractComponent, ExtractComponentPlugin, UniformComponentPlugin}, + prelude::Camera, + render_graph::RenderGraph, + render_resource::*, + renderer::RenderDevice, + texture::BevyDefault, + view::{ExtractedView, ViewTarget}, + Render, RenderApp, RenderSet, +}; + +mod node; + +pub use node::CASNode; + +/// Applies a contrast adaptive sharpening (CAS) filter to the camera. +/// +/// CAS is usually used in combination with shader based anti-aliasing methods +/// such as FXAA or TAA to regain some of the lost detail from the blurring that they introduce. +/// +/// CAS is designed to adjust the amount of sharpening applied to different areas of an image +/// based on the local contrast. This can help avoid over-sharpening areas with high contrast +/// and under-sharpening areas with low contrast. +/// +/// To use this, add the [`ContrastAdaptiveSharpeningSettings`] component to a 2D or 3D camera. +#[derive(Component, Reflect, Clone)] +#[reflect(Component)] +pub struct ContrastAdaptiveSharpeningSettings { + /// Enable or disable sharpening. + pub enabled: bool, + /// Adjusts sharpening strength. Higher values increase the amount of sharpening. + /// + /// Clamped between 0.0 and 1.0. + /// + /// The default value is 0.6. + pub sharpening_strength: f32, + /// Whether to try and avoid sharpening areas that are already noisy. + /// + /// You probably shouldn't use this, and just leave it set to false. + /// You should generally apply any sort of film grain or similar effects after CAS + /// and upscaling to avoid artifacts. + pub denoise: bool, +} + +impl Default for ContrastAdaptiveSharpeningSettings { + fn default() -> Self { + ContrastAdaptiveSharpeningSettings { + enabled: true, + sharpening_strength: 0.6, + denoise: false, + } + } +} + +#[derive(Component, Default, Reflect, Clone)] +#[reflect(Component)] +pub struct DenoiseCAS(bool); + +/// The uniform struct extracted from [`ContrastAdaptiveSharpeningSettings`] attached to a [`Camera`]. +/// Will be available for use in the CAS shader. +#[doc(hidden)] +#[derive(Component, ShaderType, Clone)] +pub struct CASUniform { + sharpness: f32, +} + +impl ExtractComponent for ContrastAdaptiveSharpeningSettings { + type Query = &'static Self; + type Filter = With; + type Out = (DenoiseCAS, CASUniform); + + fn extract_component(item: QueryItem) -> Option { + if !item.enabled || item.sharpening_strength == 0.0 { + return None; + } + Some(( + DenoiseCAS(item.denoise), + CASUniform { + // above 1.0 causes extreme artifacts and fireflies + sharpness: item.sharpening_strength.clamp(0.0, 1.0), + }, + )) + } +} + +const CONTRAST_ADAPTIVE_SHARPENING_SHADER_HANDLE: HandleUntyped = + HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 6925381244141981602); + +/// Adds Support for Contrast Adaptive Sharpening (CAS). +pub struct CASPlugin; + +impl Plugin for CASPlugin { + fn build(&self, app: &mut App) { + load_internal_asset!( + app, + CONTRAST_ADAPTIVE_SHARPENING_SHADER_HANDLE, + "robust_contrast_adaptive_sharpening.wgsl", + Shader::from_wgsl + ); + + app.register_type::(); + app.add_plugin(ExtractComponentPlugin::::default()); + app.add_plugin(UniformComponentPlugin::::default()); + + let render_app = match app.get_sub_app_mut(RenderApp) { + Ok(render_app) => render_app, + Err(_) => return, + }; + render_app + .init_resource::() + .init_resource::>() + .add_systems(Render, prepare_cas_pipelines.in_set(RenderSet::Prepare)); + { + let cas_node = CASNode::new(&mut render_app.world); + let mut binding = render_app.world.resource_mut::(); + let graph = binding.get_sub_graph_mut(core_3d::graph::NAME).unwrap(); + + graph.add_node(core_3d::graph::node::CONTRAST_ADAPTIVE_SHARPENING, cas_node); + + graph.add_node_edge( + core_3d::graph::node::TONEMAPPING, + core_3d::graph::node::CONTRAST_ADAPTIVE_SHARPENING, + ); + graph.add_node_edge( + core_3d::graph::node::FXAA, + core_3d::graph::node::CONTRAST_ADAPTIVE_SHARPENING, + ); + graph.add_node_edge( + core_3d::graph::node::CONTRAST_ADAPTIVE_SHARPENING, + core_3d::graph::node::END_MAIN_PASS_POST_PROCESSING, + ); + } + { + let cas_node = CASNode::new(&mut render_app.world); + let mut binding = render_app.world.resource_mut::(); + let graph = binding.get_sub_graph_mut(core_2d::graph::NAME).unwrap(); + + graph.add_node(core_2d::graph::node::CONTRAST_ADAPTIVE_SHARPENING, cas_node); + + graph.add_node_edge( + core_2d::graph::node::TONEMAPPING, + core_2d::graph::node::CONTRAST_ADAPTIVE_SHARPENING, + ); + graph.add_node_edge( + core_2d::graph::node::FXAA, + core_2d::graph::node::CONTRAST_ADAPTIVE_SHARPENING, + ); + graph.add_node_edge( + core_2d::graph::node::CONTRAST_ADAPTIVE_SHARPENING, + core_2d::graph::node::END_MAIN_PASS_POST_PROCESSING, + ); + } + } +} + +#[derive(Resource)] +pub struct CASPipeline { + texture_bind_group: BindGroupLayout, + sampler: Sampler, +} + +impl FromWorld for CASPipeline { + fn from_world(render_world: &mut World) -> Self { + let render_device = render_world.resource::(); + let texture_bind_group = + render_device.create_bind_group_layout(&BindGroupLayoutDescriptor { + label: Some("sharpening_texture_bind_group_layout"), + entries: &[ + BindGroupLayoutEntry { + binding: 0, + visibility: ShaderStages::FRAGMENT, + ty: BindingType::Texture { + sample_type: TextureSampleType::Float { filterable: true }, + view_dimension: TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }, + BindGroupLayoutEntry { + binding: 1, + visibility: ShaderStages::FRAGMENT, + ty: BindingType::Sampler(SamplerBindingType::Filtering), + count: None, + }, + // CAS Settings + BindGroupLayoutEntry { + binding: 2, + ty: BindingType::Buffer { + ty: BufferBindingType::Uniform, + has_dynamic_offset: true, + min_binding_size: Some(CASUniform::min_size()), + }, + visibility: ShaderStages::FRAGMENT, + count: None, + }, + ], + }); + + let sampler = render_device.create_sampler(&SamplerDescriptor::default()); + + CASPipeline { + texture_bind_group, + sampler, + } + } +} + +#[derive(PartialEq, Eq, Hash, Clone, Copy)] +pub struct CASPipelineKey { + texture_format: TextureFormat, + denoise: bool, +} + +impl SpecializedRenderPipeline for CASPipeline { + type Key = CASPipelineKey; + + fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor { + let mut shader_defs = vec![]; + if key.denoise { + shader_defs.push("RCAS_DENOISE".into()); + } + RenderPipelineDescriptor { + label: Some("contrast_adaptive_sharpening".into()), + layout: vec![self.texture_bind_group.clone()], + vertex: fullscreen_shader_vertex_state(), + fragment: Some(FragmentState { + shader: CONTRAST_ADAPTIVE_SHARPENING_SHADER_HANDLE.typed(), + shader_defs, + entry_point: "fragment".into(), + targets: vec![Some(ColorTargetState { + format: key.texture_format, + blend: None, + write_mask: ColorWrites::ALL, + })], + }), + primitive: PrimitiveState::default(), + depth_stencil: None, + multisample: MultisampleState::default(), + push_constant_ranges: Vec::new(), + } + } +} + +fn prepare_cas_pipelines( + mut commands: Commands, + pipeline_cache: Res, + mut pipelines: ResMut>, + sharpening_pipeline: Res, + views: Query<(Entity, &ExtractedView, &DenoiseCAS), With>, +) { + for (entity, view, cas_settings) in &views { + let pipeline_id = pipelines.specialize( + &pipeline_cache, + &sharpening_pipeline, + CASPipelineKey { + denoise: cas_settings.0, + texture_format: if view.hdr { + ViewTarget::TEXTURE_FORMAT_HDR + } else { + TextureFormat::bevy_default() + }, + }, + ); + + commands.entity(entity).insert(ViewCASPipeline(pipeline_id)); + } +} + +#[derive(Component)] +pub struct ViewCASPipeline(CachedRenderPipelineId); diff --git a/crates/bevy_core_pipeline/src/contrast_adaptive_sharpening/node.rs b/crates/bevy_core_pipeline/src/contrast_adaptive_sharpening/node.rs new file mode 100644 index 0000000000..7a459cfee2 --- /dev/null +++ b/crates/bevy_core_pipeline/src/contrast_adaptive_sharpening/node.rs @@ -0,0 +1,125 @@ +use std::sync::Mutex; + +use crate::contrast_adaptive_sharpening::ViewCASPipeline; +use bevy_ecs::prelude::*; +use bevy_ecs::query::QueryState; +use bevy_render::{ + extract_component::{ComponentUniforms, DynamicUniformIndex}, + render_graph::{Node, NodeRunError, RenderGraphContext}, + render_resource::{ + BindGroup, BindGroupDescriptor, BindGroupEntry, BindingResource, BufferId, Operations, + PipelineCache, RenderPassColorAttachment, RenderPassDescriptor, TextureViewId, + }, + renderer::RenderContext, + view::{ExtractedView, ViewTarget}, +}; + +use super::{CASPipeline, CASUniform}; + +pub struct CASNode { + query: QueryState< + ( + &'static ViewTarget, + &'static ViewCASPipeline, + &'static DynamicUniformIndex, + ), + With, + >, + cached_bind_group: Mutex>, +} + +impl CASNode { + pub fn new(world: &mut World) -> Self { + Self { + query: QueryState::new(world), + cached_bind_group: Mutex::new(None), + } + } +} + +impl Node for CASNode { + fn update(&mut self, world: &mut World) { + self.query.update_archetypes(world); + } + + fn run( + &self, + graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + world: &World, + ) -> Result<(), NodeRunError> { + let view_entity = graph.view_entity(); + let pipeline_cache = world.resource::(); + let sharpening_pipeline = world.resource::(); + let uniforms = world.resource::>(); + + let Ok((target, pipeline, uniform_index)) = self.query.get_manual(world, view_entity) else { return Ok(()) }; + + let uniforms_id = uniforms.buffer().unwrap().id(); + let Some(uniforms) = uniforms.binding() else { return Ok(()) }; + + let pipeline = pipeline_cache.get_render_pipeline(pipeline.0).unwrap(); + + let view_target = target.post_process_write(); + let source = view_target.source; + let destination = view_target.destination; + + let mut cached_bind_group = self.cached_bind_group.lock().unwrap(); + let bind_group = match &mut *cached_bind_group { + Some((buffer_id, texture_id, bind_group)) + if source.id() == *texture_id && uniforms_id == *buffer_id => + { + bind_group + } + cached_bind_group => { + let bind_group = + render_context + .render_device() + .create_bind_group(&BindGroupDescriptor { + label: Some("cas_bind_group"), + layout: &sharpening_pipeline.texture_bind_group, + entries: &[ + BindGroupEntry { + binding: 0, + resource: BindingResource::TextureView(view_target.source), + }, + BindGroupEntry { + binding: 1, + resource: BindingResource::Sampler( + &sharpening_pipeline.sampler, + ), + }, + BindGroupEntry { + binding: 2, + resource: uniforms, + }, + ], + }); + + let (_, _, bind_group) = + cached_bind_group.insert((uniforms_id, source.id(), bind_group)); + bind_group + } + }; + + let pass_descriptor = RenderPassDescriptor { + label: Some("contrast_adaptive_sharpening"), + color_attachments: &[Some(RenderPassColorAttachment { + view: destination, + resolve_target: None, + ops: Operations::default(), + })], + depth_stencil_attachment: None, + }; + + let mut render_pass = render_context + .command_encoder() + .begin_render_pass(&pass_descriptor); + + render_pass.set_pipeline(pipeline); + render_pass.set_bind_group(0, bind_group, &[uniform_index.index()]); + render_pass.draw(0..3, 0..1); + + Ok(()) + } +} diff --git a/crates/bevy_core_pipeline/src/contrast_adaptive_sharpening/robust_contrast_adaptive_sharpening.wgsl b/crates/bevy_core_pipeline/src/contrast_adaptive_sharpening/robust_contrast_adaptive_sharpening.wgsl new file mode 100644 index 0000000000..87cec9085e --- /dev/null +++ b/crates/bevy_core_pipeline/src/contrast_adaptive_sharpening/robust_contrast_adaptive_sharpening.wgsl @@ -0,0 +1,98 @@ +// Copyright (c) 2022 Advanced Micro Devices, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import bevy_core_pipeline::fullscreen_vertex_shader + +struct CASUniforms { + sharpness: f32, +}; + +@group(0) @binding(0) +var screenTexture: texture_2d; +@group(0) @binding(1) +var samp: sampler; +@group(0) @binding(2) +var uniforms: CASUniforms; + +// This is set at the limit of providing unnatural results for sharpening. +const FSR_RCAS_LIMIT = 0.1875; +// -4.0 instead of -1.0 to avoid issues with MSAA. +const peakC = vec2(10.0, -40.0); + +// Robust Contrast Adaptive Sharpening (RCAS) +// Based on the following implementation: +// https://github.com/GPUOpen-Effects/FidelityFX-FSR2/blob/ea97a113b0f9cadf519fbcff315cc539915a3acd/src/ffx-fsr2-api/shaders/ffx_fsr1.h#L672 +// RCAS is based on the following logic. +// RCAS uses a 5 tap filter in a cross pattern (same as CAS), +// W b +// W 1 W for taps d e f +// W h +// Where 'W' is the negative lobe weight. +// output = (W*(b+d+f+h)+e)/(4*W+1) +// RCAS solves for 'W' by seeing where the signal might clip out of the {0 to 1} input range, +// 0 == (W*(b+d+f+h)+e)/(4*W+1) -> W = -e/(b+d+f+h) +// 1 == (W*(b+d+f+h)+e)/(4*W+1) -> W = (1-e)/(b+d+f+h-4) +// Then chooses the 'W' which results in no clipping, limits 'W', and multiplies by the 'sharp' amount. +// This solution above has issues with MSAA input as the steps along the gradient cause edge detection issues. +// So RCAS uses 4x the maximum and 4x the minimum (depending on equation)in place of the individual taps. +// As well as switching from 'e' to either the minimum or maximum (depending on side), to help in energy conservation. +// This stabilizes RCAS. +// RCAS does a simple highpass which is normalized against the local contrast then shaped, +// 0.25 +// 0.25 -1 0.25 +// 0.25 +// This is used as a noise detection filter, to reduce the effect of RCAS on grain, and focus on real edges. +// The CAS node runs after tonemapping, so the input will be in the range of 0 to 1. +@fragment +fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4 { + // Algorithm uses minimal 3x3 pixel neighborhood. + // b + // d e f + // h + let b = textureSample(screenTexture, samp, in.uv, vec2(0, -1)).rgb; + let d = textureSample(screenTexture, samp, in.uv, vec2(-1, 0)).rgb; + // We need the alpha value of the pixel we're working on for the output + let e = textureSample(screenTexture, samp, in.uv).rgbw; + let f = textureSample(screenTexture, samp, in.uv, vec2(1, 0)).rgb; + let h = textureSample(screenTexture, samp, in.uv, vec2(0, 1)).rgb; + // Min and max of ring. + let mn4 = min(min(b, d), min(f, h)); + let mx4 = max(max(b, d), max(f, h)); + // Limiters + // 4.0 to avoid issues with MSAA. + let hitMin = mn4 / (4.0 * mx4); + let hitMax = (peakC.x - mx4) / (peakC.y + 4.0 * mn4); + let lobeRGB = max(-hitMin, hitMax); + var lobe = max(-FSR_RCAS_LIMIT, min(0.0, max(lobeRGB.r, max(lobeRGB.g, lobeRGB.b)))) * uniforms.sharpness; +#ifdef RCAS_DENOISE + // Luma times 2. + let bL = b.b * 0.5 + (b.r * 0.5 + b.g); + let dL = d.b * 0.5 + (d.r * 0.5 + d.g); + let eL = e.b * 0.5 + (e.r * 0.5 + e.g); + let fL = f.b * 0.5 + (f.r * 0.5 + f.g); + let hL = h.b * 0.5 + (h.r * 0.5 + h.g); + // Noise detection. + var noise = 0.25 * bL + 0.25 * dL + 0.25 * fL + 0.25 * hL - eL;; + noise = saturate(abs(noise) / (max(max(bL, dL), max(fL, hL)) - min(min(bL, dL), min(fL, hL)))); + noise = 1.0 - 0.5 * noise; + // Apply noise removal. + lobe *= noise; +#endif + return vec4((lobe * b + lobe * d + lobe * f + lobe * h + e.rgb) / (4.0 * lobe + 1.0), e.w); +} diff --git a/crates/bevy_core_pipeline/src/core_2d/mod.rs b/crates/bevy_core_pipeline/src/core_2d/mod.rs index 5f866715fb..5a2250696d 100644 --- a/crates/bevy_core_pipeline/src/core_2d/mod.rs +++ b/crates/bevy_core_pipeline/src/core_2d/mod.rs @@ -13,6 +13,7 @@ pub mod graph { pub const TONEMAPPING: &str = "tonemapping"; pub const FXAA: &str = "fxaa"; pub const UPSCALING: &str = "upscaling"; + pub const CONTRAST_ADAPTIVE_SHARPENING: &str = "contrast_adaptive_sharpening"; pub const END_MAIN_PASS_POST_PROCESSING: &str = "end_main_pass_post_processing"; } } diff --git a/crates/bevy_core_pipeline/src/core_3d/mod.rs b/crates/bevy_core_pipeline/src/core_3d/mod.rs index 0a4e5d3834..4a76b710d7 100644 --- a/crates/bevy_core_pipeline/src/core_3d/mod.rs +++ b/crates/bevy_core_pipeline/src/core_3d/mod.rs @@ -18,6 +18,7 @@ pub mod graph { pub const TONEMAPPING: &str = "tonemapping"; pub const FXAA: &str = "fxaa"; pub const UPSCALING: &str = "upscaling"; + pub const CONTRAST_ADAPTIVE_SHARPENING: &str = "contrast_adaptive_sharpening"; pub const END_MAIN_PASS_POST_PROCESSING: &str = "end_main_pass_post_processing"; } } diff --git a/crates/bevy_core_pipeline/src/lib.rs b/crates/bevy_core_pipeline/src/lib.rs index cc96506c1d..d7771ef0c3 100644 --- a/crates/bevy_core_pipeline/src/lib.rs +++ b/crates/bevy_core_pipeline/src/lib.rs @@ -1,6 +1,7 @@ pub mod blit; pub mod bloom; pub mod clear_color; +pub mod contrast_adaptive_sharpening; pub mod core_2d; pub mod core_3d; pub mod fullscreen_vertex_shader; @@ -34,6 +35,7 @@ use crate::{ blit::BlitPlugin, bloom::BloomPlugin, clear_color::{ClearColor, ClearColorConfig}, + contrast_adaptive_sharpening::CASPlugin, core_2d::Core2dPlugin, core_3d::Core3dPlugin, fullscreen_vertex_shader::FULLSCREEN_SHADER_HANDLE, @@ -72,6 +74,7 @@ impl Plugin for CorePipelinePlugin { .add_plugin(TonemappingPlugin) .add_plugin(UpscalingPlugin) .add_plugin(BloomPlugin) - .add_plugin(FxaaPlugin); + .add_plugin(FxaaPlugin) + .add_plugin(CASPlugin); } } diff --git a/examples/3d/anti_aliasing.rs b/examples/3d/anti_aliasing.rs index f9c0efbc85..084a39a2eb 100644 --- a/examples/3d/anti_aliasing.rs +++ b/examples/3d/anti_aliasing.rs @@ -4,6 +4,7 @@ use std::f32::consts::PI; use bevy::{ core_pipeline::{ + contrast_adaptive_sharpening::ContrastAdaptiveSharpeningSettings, experimental::taa::{ TemporalAntiAliasBundle, TemporalAntiAliasPlugin, TemporalAntiAliasSettings, }, @@ -23,7 +24,7 @@ fn main() { .add_plugins(DefaultPlugins) .add_plugin(TemporalAntiAliasPlugin) .add_systems(Startup, setup) - .add_systems(Update, (modify_aa, update_ui)) + .add_systems(Update, (modify_aa, modify_sharpening, update_ui)) .run(); } @@ -112,12 +113,43 @@ fn modify_aa( } } +fn modify_sharpening( + keys: Res>, + mut query: Query<&mut ContrastAdaptiveSharpeningSettings>, +) { + for mut cas in &mut query { + if keys.just_pressed(KeyCode::Key0) { + cas.enabled = !cas.enabled; + } + if cas.enabled { + if keys.just_pressed(KeyCode::Minus) { + cas.sharpening_strength -= 0.1; + cas.sharpening_strength = cas.sharpening_strength.clamp(0.0, 1.0); + } + if keys.just_pressed(KeyCode::Equals) { + cas.sharpening_strength += 0.1; + cas.sharpening_strength = cas.sharpening_strength.clamp(0.0, 1.0); + } + if keys.just_pressed(KeyCode::D) { + cas.denoise = !cas.denoise; + } + } + } +} + fn update_ui( - camera: Query<(Option<&Fxaa>, Option<&TemporalAntiAliasSettings>), With>, + camera: Query< + ( + Option<&Fxaa>, + Option<&TemporalAntiAliasSettings>, + &ContrastAdaptiveSharpeningSettings, + ), + With, + >, msaa: Res, mut ui: Query<&mut Text>, ) { - let (fxaa, taa) = camera.single(); + let (fxaa, taa, cas_settings) = camera.single(); let mut ui = ui.single_mut(); let ui = &mut ui.sections[0].value; @@ -201,6 +233,21 @@ fn update_ui( ui.push_str("(T) Extreme"); } } + + if cas_settings.enabled { + ui.push_str("\n\n----------\n\n(0) Sharpening (Enabled)\n"); + ui.push_str(&format!( + "(-/+) Strength: {:.1}\n", + cas_settings.sharpening_strength + )); + if cas_settings.denoise { + ui.push_str("(D) Denoising (Enabled)\n"); + } else { + ui.push_str("(D) Denoising (Disabled)\n"); + } + } else { + ui.push_str("\n\n----------\n\n(0) Sharpening (Disabled)\n"); + } } /// Set up a simple 3D scene @@ -261,14 +308,21 @@ fn setup( }); // Camera - commands.spawn(Camera3dBundle { - camera: Camera { - hdr: true, + commands.spawn(( + Camera3dBundle { + camera: Camera { + hdr: true, + ..default() + }, + transform: Transform::from_xyz(0.7, 0.7, 1.0) + .looking_at(Vec3::new(0.0, 0.3, 0.0), Vec3::Y), ..default() }, - transform: Transform::from_xyz(0.7, 0.7, 1.0).looking_at(Vec3::new(0.0, 0.3, 0.0), Vec3::Y), - ..default() - }); + ContrastAdaptiveSharpeningSettings { + enabled: false, + ..default() + }, + )); // UI commands.spawn(