From ace4c6023bdc0509365a9650ff2859662a49115d Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Tue, 4 Jun 2024 10:07:34 -0700 Subject: [PATCH] Implement subpixel morphological antialiasing, or SMAA. (#13423) This commit implements a large subset of [*subpixel morphological antialiasing*], better known as SMAA. SMAA is a 2011 antialiasing technique that detects jaggies in an aliased image and smooths them out. Despite its age, it's been a continual staple of games for over a decade. Four quality presets are available: *low*, *medium*, *high*, and *ultra*. I set the default to *high*, on account of modern GPUs being significantly faster than they were in 2011. Like the already-implemented FXAA, SMAA works on an unaliased image. Unlike FXAA, it requires three passes: (1) edge detection; (2) blending weight calculation; (3) neighborhood blending. Each of the first two passes writes an intermediate texture for use by the next pass. The first pass also writes to a stencil buffer in order to dramatically reduce the number of pixels that the second pass has to examine. Also unlike FXAA, two built-in lookup textures are required; I bundle them into the library in compressed KTX2 format. The [reference implementation of SMAA] is in HLSL, with abundant use of preprocessor macros to achieve GLSL compatibility. Unfortunately, the reference implementation predates WGSL by over a decade, so I had to translate the HLSL to WGSL manually. As much as was reasonably possible without sacrificing readability, I tried to translate line by line, preserving comments, both to aid reviewing and to allow patches to the HLSL to more easily apply to the WGSL. Most of SMAA's features are supported, but in the interests of making this patch somewhat less huge, I skipped a few of the more exotic ones: * The temporal variant is currently unsupported. This is and has been used in shipping games, so supporting temporal SMAA would be useful follow-up work. It would, however, require some significant work on TAA to ensure compatibility, so I opted to skip it in this patch. * Depth- and chroma-based edge detection are unimplemented; only luma is. Depth is lower-quality, but faster; chroma is higher-quality, but slower. Luma is the suggested default edge detection algorithm. (Note that depth-based edge detection wouldn't work on WebGL 2 anyway, because of the Naga bug whereby depth sampling is miscompiled in GLSL. This is the same bug that prevents depth of field from working on that platform.) * Predicated thresholding is currently unsupported. * My implementation is incompatible with SSAA and MSAA, unlike the original; MSAA must be turned off to use SMAA in Bevy. I believe this feature was rarely used in practice. The `anti_aliasing` example has been updated to allow experimentation with and testing of the different SMAA quality presets. Along the way, I refactored the example's help text rendering code a bit to eliminate code repetition. SMAA is fully supported on WebGL 2. Fixes #9819. [*subpixel morphological antialiasing*]: https://www.iryoku.com/smaa/ [reference implementation of SMAA]: https://github.com/iryoku/smaa ## Changelog ### Added * Subpixel morphological antialiasing, or SMAA, is now available. To use it, add the `SmaaSettings` component to your `Camera`. ![Screenshot 2024-05-18 134311](https://github.com/bevyengine/bevy/assets/157897/ffbd611c-1b32-4491-b2e2-e410688852ee) --------- Co-authored-by: Alice Cecile --- crates/bevy_core_pipeline/src/core_2d/mod.rs | 1 + crates/bevy_core_pipeline/src/core_3d/mod.rs | 1 + crates/bevy_core_pipeline/src/lib.rs | 3 + .../src/smaa/SMAAAreaLUT.ktx2 | Bin 0 -> 36413 bytes .../src/smaa/SMAASearchLUT.ktx2 | Bin 0 -> 293 bytes crates/bevy_core_pipeline/src/smaa/mod.rs | 1070 ++++++++++++++++ crates/bevy_core_pipeline/src/smaa/smaa.wgsl | 1106 +++++++++++++++++ examples/3d/anti_aliasing.rs | 168 +-- typos.toml | 12 +- 9 files changed, 2273 insertions(+), 88 deletions(-) create mode 100644 crates/bevy_core_pipeline/src/smaa/SMAAAreaLUT.ktx2 create mode 100644 crates/bevy_core_pipeline/src/smaa/SMAASearchLUT.ktx2 create mode 100644 crates/bevy_core_pipeline/src/smaa/mod.rs create mode 100644 crates/bevy_core_pipeline/src/smaa/smaa.wgsl diff --git a/crates/bevy_core_pipeline/src/core_2d/mod.rs b/crates/bevy_core_pipeline/src/core_2d/mod.rs index 2cfe3b8e18..869356c502 100644 --- a/crates/bevy_core_pipeline/src/core_2d/mod.rs +++ b/crates/bevy_core_pipeline/src/core_2d/mod.rs @@ -20,6 +20,7 @@ pub mod graph { Bloom, Tonemapping, Fxaa, + Smaa, Upscaling, ContrastAdaptiveSharpening, EndMainPassPostProcessing, diff --git a/crates/bevy_core_pipeline/src/core_3d/mod.rs b/crates/bevy_core_pipeline/src/core_3d/mod.rs index ddc07f4951..a847607911 100644 --- a/crates/bevy_core_pipeline/src/core_3d/mod.rs +++ b/crates/bevy_core_pipeline/src/core_3d/mod.rs @@ -32,6 +32,7 @@ pub mod graph { DepthOfField, Tonemapping, Fxaa, + Smaa, Upscaling, ContrastAdaptiveSharpening, EndMainPassPostProcessing, diff --git a/crates/bevy_core_pipeline/src/lib.rs b/crates/bevy_core_pipeline/src/lib.rs index dffeff4fc4..fec38e432b 100644 --- a/crates/bevy_core_pipeline/src/lib.rs +++ b/crates/bevy_core_pipeline/src/lib.rs @@ -21,6 +21,7 @@ pub mod motion_blur; pub mod msaa_writeback; pub mod prepass; mod skybox; +pub mod smaa; mod taa; pub mod tonemapping; pub mod upscaling; @@ -60,6 +61,7 @@ use crate::{ motion_blur::MotionBlurPlugin, msaa_writeback::MsaaWritebackPlugin, prepass::{DeferredPrepass, DepthPrepass, MotionVectorPrepass, NormalPrepass}, + smaa::SmaaPlugin, tonemapping::TonemappingPlugin, upscaling::UpscalingPlugin, }; @@ -96,6 +98,7 @@ impl Plugin for CorePipelinePlugin { CASPlugin, MotionBlurPlugin, DepthOfFieldPlugin, + SmaaPlugin, )); } } diff --git a/crates/bevy_core_pipeline/src/smaa/SMAAAreaLUT.ktx2 b/crates/bevy_core_pipeline/src/smaa/SMAAAreaLUT.ktx2 new file mode 100644 index 0000000000000000000000000000000000000000..e7d160f62eead7c3a73de30ee9c5fb3da1fe552f GIT binary patch literal 36413 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 literal 0 HcmV?d00001 diff --git a/crates/bevy_core_pipeline/src/smaa/mod.rs b/crates/bevy_core_pipeline/src/smaa/mod.rs new file mode 100644 index 0000000000..96721abdd9 --- /dev/null +++ b/crates/bevy_core_pipeline/src/smaa/mod.rs @@ -0,0 +1,1070 @@ +//! Subpixel morphological antialiasing (SMAA). +//! +//! [SMAA] is a 2011 antialiasing technique that takes an aliased image and +//! smooths out the *jaggies*, making edges smoother. It's been used in numerous +//! games and has become a staple postprocessing technique. Compared to MSAA, +//! SMAA has the advantage of compatibility with deferred rendering and +//! reduction of GPU memory bandwidth. Compared to FXAA, SMAA has the advantage +//! of improved quality, but the disadvantage of reduced performance. Compared +//! to TAA, SMAA has the advantage of stability and lack of *ghosting* +//! artifacts, but has the disadvantage of not supporting temporal accumulation, +//! which have made SMAA less popular when advanced photorealistic rendering +//! features are used in recent years. +//! +//! To use SMAA, add [`SmaaSettings`] to a [`bevy_render::camera::Camera`]. In a +//! pinch, you can simply use the default settings (via the [`Default`] trait) +//! for a high-quality, high-performance appearance. When using SMAA, you will +//! likely want to turn the default MSAA off by inserting the +//! [`bevy_render::Msaa::Off`] resource into the [`App`]. +//! +//! Those who have used SMAA in other engines should be aware that Bevy doesn't +//! yet support the following more advanced features of SMAA: +//! +//! * The temporal variant. +//! +//! * Depth- and chroma-based edge detection. +//! +//! * Predicated thresholding. +//! +//! * Compatibility with SSAA and MSAA. +//! +//! [SMAA]: https://www.iryoku.com/smaa/ + +use bevy_app::{App, Plugin}; +use bevy_asset::{load_internal_asset, load_internal_binary_asset, Handle}; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{ + component::Component, + entity::Entity, + query::{QueryItem, With}, + reflect::ReflectComponent, + schedule::IntoSystemConfigs as _, + system::{lifetimeless::Read, Commands, Query, Res, ResMut, Resource}, + world::{FromWorld, World}, +}; +use bevy_math::{vec4, Vec4}; +use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +use bevy_render::{ + camera::ExtractedCamera, + extract_component::{ExtractComponent, ExtractComponentPlugin}, + render_asset::{RenderAssetUsages, RenderAssets}, + render_graph::{ + NodeRunError, RenderGraphApp as _, RenderGraphContext, ViewNode, ViewNodeRunner, + }, + render_resource::{ + binding_types::{sampler, texture_2d, uniform_buffer}, + AddressMode, BindGroup, BindGroupEntries, BindGroupLayout, BindGroupLayoutEntries, + CachedRenderPipelineId, ColorTargetState, ColorWrites, CompareFunction, DepthStencilState, + DynamicUniformBuffer, Extent3d, FilterMode, FragmentState, LoadOp, MultisampleState, + Operations, PipelineCache, PrimitiveState, RenderPassColorAttachment, + RenderPassDepthStencilAttachment, RenderPassDescriptor, RenderPipeline, + RenderPipelineDescriptor, SamplerBindingType, SamplerDescriptor, Shader, ShaderDefVal, + ShaderStages, ShaderType, SpecializedRenderPipeline, SpecializedRenderPipelines, + StencilFaceState, StencilOperation, StencilState, StoreOp, TextureDescriptor, + TextureDimension, TextureFormat, TextureSampleType, TextureUsages, TextureView, + VertexState, + }, + renderer::{RenderContext, RenderDevice, RenderQueue}, + texture::{ + BevyDefault, CachedTexture, CompressedImageFormats, GpuImage, Image, ImageFormat, + ImageSampler, ImageType, TextureCache, + }, + view::{ExtractedView, ViewTarget}, + Render, RenderApp, RenderSet, +}; +use bevy_utils::prelude::default; + +use crate::{ + core_2d::graph::{Core2d, Node2d}, + core_3d::graph::{Core3d, Node3d}, +}; + +/// The handle of the `smaa.wgsl` shader. +const SMAA_SHADER_HANDLE: Handle = Handle::weak_from_u128(12247928498010601081); +/// The handle of the area LUT, a KTX2 format texture that SMAA uses internally. +const SMAA_AREA_LUT_TEXTURE_HANDLE: Handle = Handle::weak_from_u128(15283551734567401670); +/// The handle of the search LUT, a KTX2 format texture that SMAA uses internally. +const SMAA_SEARCH_LUT_TEXTURE_HANDLE: Handle = Handle::weak_from_u128(3187314362190283210); + +/// Adds support for subpixel morphological antialiasing, or SMAA. +pub struct SmaaPlugin; + +/// Add this component to a [`bevy_render::camera::Camera`] to enable subpixel +/// morphological antialiasing (SMAA). +#[derive(Clone, Copy, Default, Component, Reflect, ExtractComponent)] +#[reflect(Component, Default)] +pub struct SmaaSettings { + /// A predefined set of SMAA parameters: i.e. a quality level. + /// + /// Generally, you can leave this at its default level. + pub preset: SmaaPreset, +} + +/// A preset quality level for SMAA. +/// +/// Higher values are slower but result in a higher-quality image. +/// +/// The default value is *high*. +#[derive(Clone, Copy, Reflect, Default, PartialEq, Eq, Hash)] +#[reflect(Default)] +pub enum SmaaPreset { + /// Four search steps; no diagonal or corner detection. + Low, + + /// Eight search steps; no diagonal or corner detection. + Medium, + + /// Sixteen search steps, 8 diagonal search steps, and corner detection. + /// + /// This is the default. + #[default] + High, + + /// Thirty-two search steps, 8 diagonal search steps, and corner detection. + Ultra, +} + +/// A render world resource that holds all render pipeline data needed for SMAA. +/// +/// There are three separate passes, so we need three separate pipelines. +#[derive(Resource)] +pub struct SmaaPipelines { + /// Pass 1: Edge detection. + edge_detection: SmaaEdgeDetectionPipeline, + /// Pass 2: Blending weight calculation. + blending_weight_calculation: SmaaBlendingWeightCalculationPipeline, + /// Pass 3: Neighborhood blending. + neighborhood_blending: SmaaNeighborhoodBlendingPipeline, +} + +/// The pipeline data for phase 1 of SMAA: edge detection. +struct SmaaEdgeDetectionPipeline { + /// The bind group layout common to all passes. + postprocess_bind_group_layout: BindGroupLayout, + /// The bind group layout for data specific to this pass. + edge_detection_bind_group_layout: BindGroupLayout, +} + +/// The pipeline data for phase 2 of SMAA: blending weight calculation. +struct SmaaBlendingWeightCalculationPipeline { + /// The bind group layout common to all passes. + postprocess_bind_group_layout: BindGroupLayout, + /// The bind group layout for data specific to this pass. + blending_weight_calculation_bind_group_layout: BindGroupLayout, +} + +/// The pipeline data for phase 3 of SMAA: neighborhood blending. +struct SmaaNeighborhoodBlendingPipeline { + /// The bind group layout common to all passes. + postprocess_bind_group_layout: BindGroupLayout, + /// The bind group layout for data specific to this pass. + neighborhood_blending_bind_group_layout: BindGroupLayout, +} + +/// A unique identifier for a set of SMAA pipelines. +#[derive(Clone, PartialEq, Eq, Hash)] +pub struct SmaaNeighborhoodBlendingPipelineKey { + /// The format of the framebuffer. + texture_format: TextureFormat, + /// The quality preset. + preset: SmaaPreset, +} + +/// A render world component that holds the pipeline IDs for the SMAA passes. +/// +/// There are three separate SMAA passes, each with a different shader and bind +/// group layout, so we need three pipeline IDs. +#[derive(Component)] +pub struct ViewSmaaPipelines { + /// The pipeline ID for edge detection (phase 1). + edge_detection_pipeline_id: CachedRenderPipelineId, + /// The pipeline ID for blending weight calculation (phase 2). + blending_weight_calculation_pipeline_id: CachedRenderPipelineId, + /// The pipeline ID for neighborhood blending (phase 3). + neighborhood_blending_pipeline_id: CachedRenderPipelineId, +} + +/// The render graph node that performs subpixel morphological antialiasing +/// (SMAA). +#[derive(Default)] +pub struct SmaaNode; + +/// Values supplied to the GPU for SMAA. +/// +/// Currently, this just contains the render target metrics and values derived +/// from them. These could be computed by the shader itself, but the original +/// SMAA HLSL code supplied them in a uniform, so we do the same for +/// consistency. +#[derive(Clone, Copy, ShaderType)] +pub struct SmaaInfoUniform { + /// Information about the width and height of the framebuffer. + /// + /// * *x*: The reciprocal pixel width of the framebuffer. + /// + /// * *y*: The reciprocal pixel height of the framebuffer. + /// + /// * *z*: The pixel width of the framebuffer. + /// + /// * *w*: The pixel height of the framebuffer. + pub rt_metrics: Vec4, +} + +/// A render world component that stores the offset of each [`SmaaInfoUniform`] +/// within the [`SmaaInfoUniformBuffer`] for each view. +#[derive(Clone, Copy, Deref, DerefMut, Component)] +pub struct SmaaInfoUniformOffset(pub u32); + +/// The GPU buffer that holds all [`SmaaInfoUniform`]s for all views. +/// +/// This is a resource stored in the render world. +#[derive(Resource, Default, Deref, DerefMut)] +pub struct SmaaInfoUniformBuffer(pub DynamicUniformBuffer); + +/// A render world component that holds the intermediate textures necessary to +/// perform SMAA. +/// +/// This is stored on each view that has enabled SMAA. +#[derive(Component)] +pub struct SmaaTextures { + /// The two-channel texture that stores the output from the first pass (edge + /// detection). + /// + /// The second pass (blending weight calculation) reads this texture to do + /// its work. + pub edge_detection_color_texture: CachedTexture, + + /// The 8-bit stencil texture that records which pixels the first pass + /// touched, so that the second pass doesn't have to examine other pixels. + /// + /// Each texel will contain a 0 if the first pass didn't touch the + /// corresponding pixel or a 1 if the first pass did touch that pixel. + pub edge_detection_stencil_texture: CachedTexture, + + /// A four-channel RGBA texture that stores the output from the second pass + /// (blending weight calculation). + /// + /// The final pass (neighborhood blending) reads this texture to do its + /// work. + pub blend_texture: CachedTexture, +} + +/// A render world component that stores the bind groups necessary to perform +/// SMAA. +/// +/// This is stored on each view. +#[derive(Component)] +pub struct SmaaBindGroups { + /// The bind group for the first pass (edge detection). + pub edge_detection_bind_group: BindGroup, + /// The bind group for the second pass (blending weight calculation). + pub blending_weight_calculation_bind_group: BindGroup, + /// The bind group for the final pass (neighborhood blending). + pub neighborhood_blending_bind_group: BindGroup, +} + +/// Stores the specialized render pipelines for SMAA. +/// +/// Because SMAA uses three passes, we need three separate render pipeline +/// stores. +#[derive(Resource, Default)] +pub struct SmaaSpecializedRenderPipelines { + /// Specialized render pipelines for the first phase (edge detection). + edge_detection: SpecializedRenderPipelines, + + /// Specialized render pipelines for the second phase (blending weight + /// calculation). + blending_weight_calculation: SpecializedRenderPipelines, + + /// Specialized render pipelines for the third phase (neighborhood + /// blending). + neighborhood_blending: SpecializedRenderPipelines, +} + +impl Plugin for SmaaPlugin { + fn build(&self, app: &mut App) { + // Load the shader. + load_internal_asset!(app, SMAA_SHADER_HANDLE, "smaa.wgsl", Shader::from_wgsl); + + // Load the two lookup textures. These are compressed textures in KTX2 + // format. + load_internal_binary_asset!( + app, + SMAA_AREA_LUT_TEXTURE_HANDLE, + "SMAAAreaLUT.ktx2", + |bytes, _: String| Image::from_buffer( + #[cfg(all(debug_assertions, feature = "dds"))] + "SMAAAreaLUT".to_owned(), + bytes, + ImageType::Format(ImageFormat::Ktx2), + CompressedImageFormats::NONE, + false, + ImageSampler::Default, + RenderAssetUsages::RENDER_WORLD, + ) + .expect("Failed to load SMAA area LUT") + ); + + load_internal_binary_asset!( + app, + SMAA_SEARCH_LUT_TEXTURE_HANDLE, + "SMAASearchLUT.ktx2", + |bytes, _: String| Image::from_buffer( + #[cfg(all(debug_assertions, feature = "dds"))] + "SMAASearchLUT".to_owned(), + bytes, + ImageType::Format(ImageFormat::Ktx2), + CompressedImageFormats::NONE, + false, + ImageSampler::Default, + RenderAssetUsages::RENDER_WORLD, + ) + .expect("Failed to load SMAA search LUT") + ); + + app.add_plugins(ExtractComponentPlugin::::default()) + .register_type::(); + + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app + .init_resource::() + .init_resource::() + .add_systems( + Render, + ( + prepare_smaa_pipelines.in_set(RenderSet::Prepare), + prepare_smaa_uniforms.in_set(RenderSet::PrepareResources), + prepare_smaa_textures.in_set(RenderSet::PrepareResources), + prepare_smaa_bind_groups.in_set(RenderSet::PrepareBindGroups), + ), + ) + .add_render_graph_node::>(Core3d, Node3d::Smaa) + .add_render_graph_edges( + Core3d, + ( + Node3d::Tonemapping, + Node3d::Smaa, + Node3d::EndMainPassPostProcessing, + ), + ) + .add_render_graph_node::>(Core2d, Node2d::Smaa) + .add_render_graph_edges( + Core2d, + ( + Node2d::Tonemapping, + Node2d::Smaa, + Node2d::EndMainPassPostProcessing, + ), + ); + } + + fn finish(&self, app: &mut App) { + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { + render_app.init_resource::(); + } + } +} + +impl FromWorld for SmaaPipelines { + fn from_world(world: &mut World) -> Self { + let render_device = world.resource::(); + + // Create the postprocess bind group layout (all passes, bind group 0). + let postprocess_bind_group_layout = render_device.create_bind_group_layout( + "SMAA postprocess bind group layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::FRAGMENT, + ( + texture_2d(TextureSampleType::Float { filterable: true }), + uniform_buffer::(true) + .visibility(ShaderStages::VERTEX_FRAGMENT), + ), + ), + ); + + // Create the edge detection bind group layout (pass 1, bind group 1). + let edge_detection_bind_group_layout = render_device.create_bind_group_layout( + "SMAA edge detection bind group layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::FRAGMENT, + (sampler(SamplerBindingType::Filtering),), + ), + ); + + // Create the blending weight calculation bind group layout (pass 2, bind group 1). + let blending_weight_calculation_bind_group_layout = render_device.create_bind_group_layout( + "SMAA blending weight calculation bind group layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::FRAGMENT, + ( + texture_2d(TextureSampleType::Float { filterable: true }), // edges texture + sampler(SamplerBindingType::Filtering), // edges sampler + texture_2d(TextureSampleType::Float { filterable: true }), // search texture + texture_2d(TextureSampleType::Float { filterable: true }), // area texture + ), + ), + ); + + // Create the neighborhood blending bind group layout (pass 3, bind group 1). + let neighborhood_blending_bind_group_layout = render_device.create_bind_group_layout( + "SMAA neighborhood blending bind group layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::FRAGMENT, + ( + texture_2d(TextureSampleType::Float { filterable: true }), + sampler(SamplerBindingType::Filtering), + ), + ), + ); + + SmaaPipelines { + edge_detection: SmaaEdgeDetectionPipeline { + postprocess_bind_group_layout: postprocess_bind_group_layout.clone(), + edge_detection_bind_group_layout, + }, + blending_weight_calculation: SmaaBlendingWeightCalculationPipeline { + postprocess_bind_group_layout: postprocess_bind_group_layout.clone(), + blending_weight_calculation_bind_group_layout, + }, + neighborhood_blending: SmaaNeighborhoodBlendingPipeline { + postprocess_bind_group_layout, + neighborhood_blending_bind_group_layout, + }, + } + } +} + +// Phase 1: edge detection. +impl SpecializedRenderPipeline for SmaaEdgeDetectionPipeline { + type Key = SmaaPreset; + + fn specialize(&self, preset: Self::Key) -> RenderPipelineDescriptor { + let shader_defs = vec!["SMAA_EDGE_DETECTION".into(), preset.shader_def()]; + + // We mark the pixels that we touched with a 1 so that the blending + // weight calculation (phase 2) will only consider those. This reduces + // the overhead of phase 2 considerably. + let stencil_face_state = StencilFaceState { + compare: CompareFunction::Always, + fail_op: StencilOperation::Replace, + depth_fail_op: StencilOperation::Replace, + pass_op: StencilOperation::Replace, + }; + + RenderPipelineDescriptor { + label: Some("SMAA edge detection".into()), + layout: vec![ + self.postprocess_bind_group_layout.clone(), + self.edge_detection_bind_group_layout.clone(), + ], + vertex: VertexState { + shader: SMAA_SHADER_HANDLE, + shader_defs: shader_defs.clone(), + entry_point: "edge_detection_vertex_main".into(), + buffers: vec![], + }, + fragment: Some(FragmentState { + shader: SMAA_SHADER_HANDLE, + shader_defs, + entry_point: "luma_edge_detection_fragment_main".into(), + targets: vec![Some(ColorTargetState { + format: TextureFormat::Rg8Unorm, + blend: None, + write_mask: ColorWrites::ALL, + })], + }), + push_constant_ranges: vec![], + primitive: PrimitiveState::default(), + depth_stencil: Some(DepthStencilState { + format: TextureFormat::Stencil8, + depth_write_enabled: false, + depth_compare: CompareFunction::Always, + stencil: StencilState { + front: stencil_face_state, + back: stencil_face_state, + read_mask: 1, + write_mask: 1, + }, + bias: default(), + }), + multisample: MultisampleState::default(), + } + } +} + +// Phase 2: blending weight calculation. +impl SpecializedRenderPipeline for SmaaBlendingWeightCalculationPipeline { + type Key = SmaaPreset; + + fn specialize(&self, preset: Self::Key) -> RenderPipelineDescriptor { + let shader_defs = vec![ + "SMAA_BLENDING_WEIGHT_CALCULATION".into(), + preset.shader_def(), + ]; + + // Only consider the pixels that were touched in phase 1. + let stencil_face_state = StencilFaceState { + compare: CompareFunction::Equal, + fail_op: StencilOperation::Keep, + depth_fail_op: StencilOperation::Keep, + pass_op: StencilOperation::Keep, + }; + + RenderPipelineDescriptor { + label: Some("SMAA blending weight calculation".into()), + layout: vec![ + self.postprocess_bind_group_layout.clone(), + self.blending_weight_calculation_bind_group_layout.clone(), + ], + vertex: VertexState { + shader: SMAA_SHADER_HANDLE, + shader_defs: shader_defs.clone(), + entry_point: "blending_weight_calculation_vertex_main".into(), + buffers: vec![], + }, + fragment: Some(FragmentState { + shader: SMAA_SHADER_HANDLE, + shader_defs, + entry_point: "blending_weight_calculation_fragment_main".into(), + targets: vec![Some(ColorTargetState { + format: TextureFormat::Rgba8Unorm, + blend: None, + write_mask: ColorWrites::ALL, + })], + }), + push_constant_ranges: vec![], + primitive: PrimitiveState::default(), + depth_stencil: Some(DepthStencilState { + format: TextureFormat::Stencil8, + depth_write_enabled: false, + depth_compare: CompareFunction::Always, + stencil: StencilState { + front: stencil_face_state, + back: stencil_face_state, + read_mask: 1, + write_mask: 1, + }, + bias: default(), + }), + multisample: MultisampleState::default(), + } + } +} + +impl SpecializedRenderPipeline for SmaaNeighborhoodBlendingPipeline { + type Key = SmaaNeighborhoodBlendingPipelineKey; + + fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor { + let shader_defs = vec!["SMAA_NEIGHBORHOOD_BLENDING".into(), key.preset.shader_def()]; + + RenderPipelineDescriptor { + label: Some("SMAA neighborhood blending".into()), + layout: vec![ + self.postprocess_bind_group_layout.clone(), + self.neighborhood_blending_bind_group_layout.clone(), + ], + vertex: VertexState { + shader: SMAA_SHADER_HANDLE, + shader_defs: shader_defs.clone(), + entry_point: "neighborhood_blending_vertex_main".into(), + buffers: vec![], + }, + fragment: Some(FragmentState { + shader: SMAA_SHADER_HANDLE, + shader_defs, + entry_point: "neighborhood_blending_fragment_main".into(), + targets: vec![Some(ColorTargetState { + format: key.texture_format, + blend: None, + write_mask: ColorWrites::ALL, + })], + }), + push_constant_ranges: vec![], + primitive: PrimitiveState::default(), + depth_stencil: None, + multisample: MultisampleState::default(), + } + } +} + +/// A system, part of the render app, that specializes the three pipelines +/// needed for SMAA according to each view's SMAA settings. +fn prepare_smaa_pipelines( + mut commands: Commands, + pipeline_cache: Res, + mut specialized_render_pipelines: ResMut, + smaa_pipelines: Res, + view_targets: Query<(Entity, &ExtractedView, &SmaaSettings)>, +) { + for (entity, view, settings) in &view_targets { + let edge_detection_pipeline_id = specialized_render_pipelines.edge_detection.specialize( + &pipeline_cache, + &smaa_pipelines.edge_detection, + settings.preset, + ); + + let blending_weight_calculation_pipeline_id = specialized_render_pipelines + .blending_weight_calculation + .specialize( + &pipeline_cache, + &smaa_pipelines.blending_weight_calculation, + settings.preset, + ); + + let neighborhood_blending_pipeline_id = specialized_render_pipelines + .neighborhood_blending + .specialize( + &pipeline_cache, + &smaa_pipelines.neighborhood_blending, + SmaaNeighborhoodBlendingPipelineKey { + texture_format: if view.hdr { + ViewTarget::TEXTURE_FORMAT_HDR + } else { + TextureFormat::bevy_default() + }, + preset: settings.preset, + }, + ); + + commands.entity(entity).insert(ViewSmaaPipelines { + edge_detection_pipeline_id, + blending_weight_calculation_pipeline_id, + neighborhood_blending_pipeline_id, + }); + } +} + +/// A system, part of the render app, that builds the [`SmaaInfoUniform`] data +/// for each view with SMAA enabled and writes the resulting data to GPU memory. +fn prepare_smaa_uniforms( + mut commands: Commands, + render_device: Res, + render_queue: Res, + view_targets: Query<(Entity, &ExtractedView), With>, + mut smaa_info_buffer: ResMut, +) { + smaa_info_buffer.clear(); + for (entity, view) in &view_targets { + let offset = smaa_info_buffer.push(&SmaaInfoUniform { + rt_metrics: vec4( + 1.0 / view.viewport.z as f32, + 1.0 / view.viewport.w as f32, + view.viewport.z as f32, + view.viewport.w as f32, + ), + }); + commands + .entity(entity) + .insert(SmaaInfoUniformOffset(offset)); + } + + smaa_info_buffer.write_buffer(&render_device, &render_queue); +} + +/// A system, part of the render app, that builds the intermediate textures for +/// each view with SMAA enabled. +/// +/// Phase 1 (edge detection) needs a two-channel RG texture and an 8-bit stencil +/// texture; phase 2 (blend weight calculation) needs a four-channel RGBA +/// texture. +fn prepare_smaa_textures( + mut commands: Commands, + render_device: Res, + mut texture_cache: ResMut, + view_targets: Query<(Entity, &ExtractedCamera), (With, With)>, +) { + for (entity, camera) in &view_targets { + let Some(texture_size) = camera.physical_target_size else { + continue; + }; + + let texture_size = Extent3d { + width: texture_size.x, + height: texture_size.y, + depth_or_array_layers: 1, + }; + + // Create the two-channel RG texture for phase 1 (edge detection). + let edge_detection_color_texture = texture_cache.get( + &render_device, + TextureDescriptor { + label: Some("SMAA edge detection color texture"), + size: texture_size, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: TextureFormat::Rg8Unorm, + usage: TextureUsages::TEXTURE_BINDING | TextureUsages::RENDER_ATTACHMENT, + view_formats: &[], + }, + ); + + // Create the stencil texture for phase 1 (edge detection). + let edge_detection_stencil_texture = texture_cache.get( + &render_device, + TextureDescriptor { + label: Some("SMAA edge detection stencil texture"), + size: texture_size, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: TextureFormat::Stencil8, + usage: TextureUsages::RENDER_ATTACHMENT, + view_formats: &[], + }, + ); + + // Create the four-channel RGBA texture for phase 2 (blending weight + // calculation). + let blend_texture = texture_cache.get( + &render_device, + TextureDescriptor { + label: Some("SMAA blend texture"), + size: texture_size, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: TextureFormat::Rgba8Unorm, + usage: TextureUsages::TEXTURE_BINDING | TextureUsages::RENDER_ATTACHMENT, + view_formats: &[], + }, + ); + + commands.entity(entity).insert(SmaaTextures { + edge_detection_color_texture, + edge_detection_stencil_texture, + blend_texture, + }); + } +} + +/// A system, part of the render app, that builds the SMAA bind groups for each +/// view with SMAA enabled. +fn prepare_smaa_bind_groups( + mut commands: Commands, + render_device: Res, + smaa_pipelines: Res, + images: Res>, + view_targets: Query<(Entity, &SmaaTextures), (With, With)>, +) { + // Fetch the two lookup textures. These are bundled in this library. + let (Some(search_texture), Some(area_texture)) = ( + images.get(&SMAA_SEARCH_LUT_TEXTURE_HANDLE), + images.get(&SMAA_AREA_LUT_TEXTURE_HANDLE), + ) else { + return; + }; + + for (entity, smaa_textures) in &view_targets { + // We use the same sampler settings for all textures, so we can build + // only one and reuse it. + let sampler = render_device.create_sampler(&SamplerDescriptor { + label: Some("SMAA sampler"), + address_mode_u: AddressMode::ClampToEdge, + address_mode_v: AddressMode::ClampToEdge, + address_mode_w: AddressMode::ClampToEdge, + mag_filter: FilterMode::Linear, + min_filter: FilterMode::Linear, + ..default() + }); + + commands.entity(entity).insert(SmaaBindGroups { + edge_detection_bind_group: render_device.create_bind_group( + Some("SMAA edge detection bind group"), + &smaa_pipelines + .edge_detection + .edge_detection_bind_group_layout, + &BindGroupEntries::sequential((&sampler,)), + ), + blending_weight_calculation_bind_group: render_device.create_bind_group( + Some("SMAA blending weight calculation bind group"), + &smaa_pipelines + .blending_weight_calculation + .blending_weight_calculation_bind_group_layout, + &BindGroupEntries::sequential(( + &smaa_textures.edge_detection_color_texture.default_view, + &sampler, + &search_texture.texture_view, + &area_texture.texture_view, + )), + ), + neighborhood_blending_bind_group: render_device.create_bind_group( + Some("SMAA neighborhood blending bind group"), + &smaa_pipelines + .neighborhood_blending + .neighborhood_blending_bind_group_layout, + &BindGroupEntries::sequential(( + &smaa_textures.blend_texture.default_view, + &sampler, + )), + ), + }); + } +} + +impl ViewNode for SmaaNode { + type ViewQuery = ( + Read, + Read, + Read, + Read, + Read, + ); + + fn run<'w>( + &self, + _: &mut RenderGraphContext, + render_context: &mut RenderContext<'w>, + ( + view_target, + view_pipelines, + view_smaa_uniform_offset, + smaa_textures, + view_smaa_bind_groups, + ): QueryItem<'w, Self::ViewQuery>, + world: &'w World, + ) -> Result<(), NodeRunError> { + let pipeline_cache = world.resource::(); + let smaa_pipelines = world.resource::(); + let smaa_info_uniform_buffer = world.resource::(); + + // Fetch the render pipelines. + let ( + Some(edge_detection_pipeline), + Some(blending_weight_calculation_pipeline), + Some(neighborhood_blending_pipeline), + ) = ( + pipeline_cache.get_render_pipeline(view_pipelines.edge_detection_pipeline_id), + pipeline_cache + .get_render_pipeline(view_pipelines.blending_weight_calculation_pipeline_id), + pipeline_cache.get_render_pipeline(view_pipelines.neighborhood_blending_pipeline_id), + ) + else { + return Ok(()); + }; + + // Fetch the framebuffer textures. + let postprocess = view_target.post_process_write(); + let (source, destination) = (postprocess.source, postprocess.destination); + + // Stage 1: Edge detection pass. + perform_edge_detection( + render_context, + smaa_pipelines, + smaa_textures, + view_smaa_bind_groups, + smaa_info_uniform_buffer, + view_smaa_uniform_offset, + edge_detection_pipeline, + source, + ); + + // Stage 2: Blending weight calculation pass. + perform_blending_weight_calculation( + render_context, + smaa_pipelines, + smaa_textures, + view_smaa_bind_groups, + smaa_info_uniform_buffer, + view_smaa_uniform_offset, + blending_weight_calculation_pipeline, + source, + ); + + // Stage 3: Neighborhood blending pass. + perform_neighborhood_blending( + render_context, + smaa_pipelines, + view_smaa_bind_groups, + smaa_info_uniform_buffer, + view_smaa_uniform_offset, + neighborhood_blending_pipeline, + source, + destination, + ); + + Ok(()) + } +} + +/// Performs edge detection (phase 1). +/// +/// This runs as part of the [`SmaaNode`]. It reads from the source texture and +/// writes to the two-channel RG edges texture. Additionally, it ensures that +/// all pixels it didn't touch are stenciled out so that phase 2 won't have to +/// examine them. +#[allow(clippy::too_many_arguments)] +fn perform_edge_detection( + render_context: &mut RenderContext, + smaa_pipelines: &SmaaPipelines, + smaa_textures: &SmaaTextures, + view_smaa_bind_groups: &SmaaBindGroups, + smaa_info_uniform_buffer: &SmaaInfoUniformBuffer, + view_smaa_uniform_offset: &SmaaInfoUniformOffset, + edge_detection_pipeline: &RenderPipeline, + source: &TextureView, +) { + // Create the edge detection bind group. + let postprocess_bind_group = render_context.render_device().create_bind_group( + None, + &smaa_pipelines.edge_detection.postprocess_bind_group_layout, + &BindGroupEntries::sequential((source, &**smaa_info_uniform_buffer)), + ); + + // Create the edge detection pass descriptor. + let pass_descriptor = RenderPassDescriptor { + label: Some("SMAA edge detection pass"), + color_attachments: &[Some(RenderPassColorAttachment { + view: &smaa_textures.edge_detection_color_texture.default_view, + resolve_target: None, + ops: default(), + })], + depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { + view: &smaa_textures.edge_detection_stencil_texture.default_view, + depth_ops: None, + stencil_ops: Some(Operations { + load: LoadOp::Clear(0), + store: StoreOp::Store, + }), + }), + timestamp_writes: None, + occlusion_query_set: None, + }; + + // Run the actual render pass. + let mut render_pass = render_context + .command_encoder() + .begin_render_pass(&pass_descriptor); + render_pass.set_pipeline(edge_detection_pipeline); + render_pass.set_bind_group(0, &postprocess_bind_group, &[**view_smaa_uniform_offset]); + render_pass.set_bind_group(1, &view_smaa_bind_groups.edge_detection_bind_group, &[]); + render_pass.set_stencil_reference(1); + render_pass.draw(0..3, 0..1); +} + +/// Performs blending weight calculation (phase 2). +/// +/// This runs as part of the [`SmaaNode`]. It reads the edges texture and writes +/// to the blend weight texture, using the stencil buffer to avoid processing +/// pixels it doesn't need to examine. +#[allow(clippy::too_many_arguments)] +fn perform_blending_weight_calculation( + render_context: &mut RenderContext, + smaa_pipelines: &SmaaPipelines, + smaa_textures: &SmaaTextures, + view_smaa_bind_groups: &SmaaBindGroups, + smaa_info_uniform_buffer: &SmaaInfoUniformBuffer, + view_smaa_uniform_offset: &SmaaInfoUniformOffset, + blending_weight_calculation_pipeline: &RenderPipeline, + source: &TextureView, +) { + // Create the blending weight calculation bind group. + let postprocess_bind_group = render_context.render_device().create_bind_group( + None, + &smaa_pipelines + .blending_weight_calculation + .postprocess_bind_group_layout, + &BindGroupEntries::sequential((source, &**smaa_info_uniform_buffer)), + ); + + // Create the blending weight calculation pass descriptor. + let pass_descriptor = RenderPassDescriptor { + label: Some("SMAA blending weight calculation pass"), + color_attachments: &[Some(RenderPassColorAttachment { + view: &smaa_textures.blend_texture.default_view, + resolve_target: None, + ops: default(), + })], + depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { + view: &smaa_textures.edge_detection_stencil_texture.default_view, + depth_ops: None, + stencil_ops: Some(Operations { + load: LoadOp::Load, + store: StoreOp::Discard, + }), + }), + timestamp_writes: None, + occlusion_query_set: None, + }; + + // Run the actual render pass. + let mut render_pass = render_context + .command_encoder() + .begin_render_pass(&pass_descriptor); + render_pass.set_pipeline(blending_weight_calculation_pipeline); + render_pass.set_bind_group(0, &postprocess_bind_group, &[**view_smaa_uniform_offset]); + render_pass.set_bind_group( + 1, + &view_smaa_bind_groups.blending_weight_calculation_bind_group, + &[], + ); + render_pass.set_stencil_reference(1); + render_pass.draw(0..3, 0..1); +} + +/// Performs blending weight calculation (phase 3). +/// +/// This runs as part of the [`SmaaNode`]. It reads from the blend weight +/// texture. It's the only phase that writes to the postprocessing destination. +#[allow(clippy::too_many_arguments)] +fn perform_neighborhood_blending( + render_context: &mut RenderContext, + smaa_pipelines: &SmaaPipelines, + view_smaa_bind_groups: &SmaaBindGroups, + smaa_info_uniform_buffer: &SmaaInfoUniformBuffer, + view_smaa_uniform_offset: &SmaaInfoUniformOffset, + neighborhood_blending_pipeline: &RenderPipeline, + source: &TextureView, + destination: &TextureView, +) { + let postprocess_bind_group = render_context.render_device().create_bind_group( + None, + &smaa_pipelines + .neighborhood_blending + .postprocess_bind_group_layout, + &BindGroupEntries::sequential((source, &**smaa_info_uniform_buffer)), + ); + + let pass_descriptor = RenderPassDescriptor { + label: Some("SMAA neighborhood blending pass"), + color_attachments: &[Some(RenderPassColorAttachment { + view: destination, + resolve_target: None, + ops: default(), + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }; + + let mut neighborhood_blending_render_pass = render_context + .command_encoder() + .begin_render_pass(&pass_descriptor); + neighborhood_blending_render_pass.set_pipeline(neighborhood_blending_pipeline); + neighborhood_blending_render_pass.set_bind_group( + 0, + &postprocess_bind_group, + &[**view_smaa_uniform_offset], + ); + neighborhood_blending_render_pass.set_bind_group( + 1, + &view_smaa_bind_groups.neighborhood_blending_bind_group, + &[], + ); + neighborhood_blending_render_pass.draw(0..3, 0..1); +} + +impl SmaaPreset { + /// Returns the `#define` in the shader corresponding to this quality + /// preset. + fn shader_def(&self) -> ShaderDefVal { + match *self { + SmaaPreset::Low => "SMAA_PRESET_LOW".into(), + SmaaPreset::Medium => "SMAA_PRESET_MEDIUM".into(), + SmaaPreset::High => "SMAA_PRESET_HIGH".into(), + SmaaPreset::Ultra => "SMAA_PRESET_ULTRA".into(), + } + } +} diff --git a/crates/bevy_core_pipeline/src/smaa/smaa.wgsl b/crates/bevy_core_pipeline/src/smaa/smaa.wgsl new file mode 100644 index 0000000000..f8f23c987b --- /dev/null +++ b/crates/bevy_core_pipeline/src/smaa/smaa.wgsl @@ -0,0 +1,1106 @@ +/** + * Copyright (C) 2013 Jorge Jimenez (jorge@iryoku.com) + * Copyright (C) 2013 Jose I. Echevarria (joseignacioechevarria@gmail.com) + * Copyright (C) 2013 Belen Masia (bmasia@unizar.es) + * Copyright (C) 2013 Fernando Navarro (fernandn@microsoft.com) + * Copyright (C) 2013 Diego Gutierrez (diegog@unizar.es) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * 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. As clarification, there + * is no requirement that the copyright notice and permission be included in + * binary distributions 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. + */ + +/** + * _______ ___ ___ ___ ___ + * / || \/ | / \ / \ + * | (---- | \ / | / ^ \ / ^ \ + * \ \ | |\/| | / /_\ \ / /_\ \ + * ----) | | | | | / _____ \ / _____ \ + * |_______/ |__| |__| /__/ \__\ /__/ \__\ + * + * E N H A N C E D + * S U B P I X E L M O R P H O L O G I C A L A N T I A L I A S I N G + * + * http://www.iryoku.com/smaa/ + * + * Hi, welcome aboard! + * + * Here you'll find instructions to get the shader up and running as fast as + * possible. + * + * IMPORTANTE NOTICE: when updating, remember to update both this file and the + * precomputed textures! They may change from version to version. + * + * The shader has three passes, chained together as follows: + * + * |input|------------------� + * v | + * [ SMAA*EdgeDetection ] | + * v | + * |edgesTex| | + * v | + * [ SMAABlendingWeightCalculation ] | + * v | + * |blendTex| | + * v | + * [ SMAANeighborhoodBlending ] <------� + * v + * |output| + * + * Note that each [pass] has its own vertex and pixel shader. Remember to use + * oversized triangles instead of quads to avoid overshading along the + * diagonal. + * + * You've three edge detection methods to choose from: luma, color or depth. + * They represent different quality/performance and anti-aliasing/sharpness + * tradeoffs, so our recommendation is for you to choose the one that best + * suits your particular scenario: + * + * - Depth edge detection is usually the fastest but it may miss some edges. + * + * - Luma edge detection is usually more expensive than depth edge detection, + * but catches visible edges that depth edge detection can miss. + * + * - Color edge detection is usually the most expensive one but catches + * chroma-only edges. + * + * For quickstarters: just use luma edge detection. + * + * The general advice is to not rush the integration process and ensure each + * step is done correctly (don't try to integrate SMAA T2x with predicated edge + * detection from the start!). Ok then, let's go! + * + * 1. The first step is to create two RGBA temporal render targets for holding + * |edgesTex| and |blendTex|. + * + * In DX10 or DX11, you can use a RG render target for the edges texture. + * In the case of NVIDIA GPUs, using RG render targets seems to actually be + * slower. + * + * On the Xbox 360, you can use the same render target for resolving both + * |edgesTex| and |blendTex|, as they aren't needed simultaneously. + * + * 2. Both temporal render targets |edgesTex| and |blendTex| must be cleared + * each frame. Do not forget to clear the alpha channel! + * + * 3. The next step is loading the two supporting precalculated textures, + * 'areaTex' and 'searchTex'. You'll find them in the 'Textures' folder as + * C++ headers, and also as regular DDS files. They'll be needed for the + * 'SMAABlendingWeightCalculation' pass. + * + * If you use the C++ headers, be sure to load them in the format specified + * inside of them. + * + * You can also compress 'areaTex' and 'searchTex' using BC5 and BC4 + * respectively, if you have that option in your content processor pipeline. + * When compressing then, you get a non-perceptible quality decrease, and a + * marginal performance increase. + * + * 4. All samplers must be set to linear filtering and clamp. + * + * After you get the technique working, remember that 64-bit inputs have + * half-rate linear filtering on GCN. + * + * If SMAA is applied to 64-bit color buffers, switching to point filtering + * when accessing them will increase the performance. Search for + * 'SMAASamplePoint' to see which textures may benefit from point + * filtering, and where (which is basically the color input in the edge + * detection and resolve passes). + * + * 5. All texture reads and buffer writes must be non-sRGB, with the exception + * of the input read and the output write in + * 'SMAANeighborhoodBlending' (and only in this pass!). If sRGB reads in + * this last pass are not possible, the technique will work anyway, but + * will perform antialiasing in gamma space. + * + * IMPORTANT: for best results the input read for the color/luma edge + * detection should *NOT* be sRGB. + * + * 6. Before including SMAA.h you'll have to setup the render target metrics, + * the target and any optional configuration defines. Optionally you can + * use a preset. + * + * You have the following targets available: + * SMAA_HLSL_3 + * SMAA_HLSL_4 + * SMAA_HLSL_4_1 + * SMAA_GLSL_3 * + * SMAA_GLSL_4 * + * + * * (See SMAA_INCLUDE_VS and SMAA_INCLUDE_PS below). + * + * And four presets: + * SMAA_PRESET_LOW (%60 of the quality) + * SMAA_PRESET_MEDIUM (%80 of the quality) + * SMAA_PRESET_HIGH (%95 of the quality) + * SMAA_PRESET_ULTRA (%99 of the quality) + * + * For example: + * #define SMAA_RT_METRICS float4(1.0 / 1280.0, 1.0 / 720.0, 1280.0, 720.0) + * #define SMAA_HLSL_4 + * #define SMAA_PRESET_HIGH + * #include "SMAA.h" + * + * Note that SMAA_RT_METRICS doesn't need to be a macro, it can be a + * uniform variable. The code is designed to minimize the impact of not + * using a constant value, but it is still better to hardcode it. + * + * Depending on how you encoded 'areaTex' and 'searchTex', you may have to + * add (and customize) the following defines before including SMAA.h: + * #define SMAA_AREATEX_SELECT(sample) sample.rg + * #define SMAA_SEARCHTEX_SELECT(sample) sample.r + * + * If your engine is already using porting macros, you can define + * SMAA_CUSTOM_SL, and define the porting functions by yourself. + * + * 7. Then, you'll have to setup the passes as indicated in the scheme above. + * You can take a look into SMAA.fx, to see how we did it for our demo. + * Checkout the function wrappers, you may want to copy-paste them! + * + * 8. It's recommended to validate the produced |edgesTex| and |blendTex|. + * You can use a screenshot from your engine to compare the |edgesTex| + * and |blendTex| produced inside of the engine with the results obtained + * with the reference demo. + * + * 9. After you get the last pass to work, it's time to optimize. You'll have + * to initialize a stencil buffer in the first pass (discard is already in + * the code), then mask execution by using it the second pass. The last + * pass should be executed in all pixels. + * + * + * After this point you can choose to enable predicated thresholding, + * temporal supersampling and motion blur integration: + * + * a) If you want to use predicated thresholding, take a look into + * SMAA_PREDICATION; you'll need to pass an extra texture in the edge + * detection pass. + * + * b) If you want to enable temporal supersampling (SMAA T2x): + * + * 1. The first step is to render using subpixel jitters. I won't go into + * detail, but it's as simple as moving each vertex position in the + * vertex shader, you can check how we do it in our DX10 demo. + * + * 2. Then, you must setup the temporal resolve. You may want to take a look + * into SMAAResolve for resolving 2x modes. After you get it working, you'll + * probably see ghosting everywhere. But fear not, you can enable the + * CryENGINE temporal reprojection by setting the SMAA_REPROJECTION macro. + * Check out SMAA_DECODE_VELOCITY if your velocity buffer is encoded. + * + * 3. The next step is to apply SMAA to each subpixel jittered frame, just as + * done for 1x. + * + * 4. At this point you should already have something usable, but for best + * results the proper area textures must be set depending on current jitter. + * For this, the parameter 'subsampleIndices' of + * 'SMAABlendingWeightCalculationPS' must be set as follows, for our T2x + * mode: + * + * @SUBSAMPLE_INDICES + * + * | S# | Camera Jitter | subsampleIndices | + * +----+------------------+---------------------+ + * | 0 | ( 0.25, -0.25) | float4(1, 1, 1, 0) | + * | 1 | (-0.25, 0.25) | float4(2, 2, 2, 0) | + * + * These jitter positions assume a bottom-to-top y axis. S# stands for the + * sample number. + * + * More information about temporal supersampling here: + * http://iryoku.com/aacourse/downloads/13-Anti-Aliasing-Methods-in-CryENGINE-3.pdf + * + * c) If you want to enable spatial multisampling (SMAA S2x): + * + * 1. The scene must be rendered using MSAA 2x. The MSAA 2x buffer must be + * created with: + * - DX10: see below (*) + * - DX10.1: D3D10_STANDARD_MULTISAMPLE_PATTERN or + * - DX11: D3D11_STANDARD_MULTISAMPLE_PATTERN + * + * This allows to ensure that the subsample order matches the table in + * @SUBSAMPLE_INDICES. + * + * (*) In the case of DX10, we refer the reader to: + * - SMAA::detectMSAAOrder and + * - SMAA::msaaReorder + * + * These functions allow to match the standard multisample patterns by + * detecting the subsample order for a specific GPU, and reordering + * them appropriately. + * + * 2. A shader must be run to output each subsample into a separate buffer + * (DX10 is required). You can use SMAASeparate for this purpose, or just do + * it in an existing pass (for example, in the tone mapping pass, which has + * the advantage of feeding tone mapped subsamples to SMAA, which will yield + * better results). + * + * 3. The full SMAA 1x pipeline must be run for each separated buffer, storing + * the results in the final buffer. The second run should alpha blend with + * the existing final buffer using a blending factor of 0.5. + * 'subsampleIndices' must be adjusted as in the SMAA T2x case (see point + * b). + * + * d) If you want to enable temporal supersampling on top of SMAA S2x + * (which actually is SMAA 4x): + * + * 1. SMAA 4x consists on temporally jittering SMAA S2x, so the first step is + * to calculate SMAA S2x for current frame. In this case, 'subsampleIndices' + * must be set as follows: + * + * | F# | S# | Camera Jitter | Net Jitter | subsampleIndices | + * +----+----+--------------------+-------------------+----------------------+ + * | 0 | 0 | ( 0.125, 0.125) | ( 0.375, -0.125) | float4(5, 3, 1, 3) | + * | 0 | 1 | ( 0.125, 0.125) | (-0.125, 0.375) | float4(4, 6, 2, 3) | + * +----+----+--------------------+-------------------+----------------------+ + * | 1 | 2 | (-0.125, -0.125) | ( 0.125, -0.375) | float4(3, 5, 1, 4) | + * | 1 | 3 | (-0.125, -0.125) | (-0.375, 0.125) | float4(6, 4, 2, 4) | + * + * These jitter positions assume a bottom-to-top y axis. F# stands for the + * frame number. S# stands for the sample number. + * + * 2. After calculating SMAA S2x for current frame (with the new subsample + * indices), previous frame must be reprojected as in SMAA T2x mode (see + * point b). + * + * e) If motion blur is used, you may want to do the edge detection pass + * together with motion blur. This has two advantages: + * + * 1. Pixels under heavy motion can be omitted from the edge detection process. + * For these pixels we can just store "no edge", as motion blur will take + * care of them. + * 2. The center pixel tap is reused. + * + * Note that in this case depth testing should be used instead of stenciling, + * as we have to write all the pixels in the motion blur pass. + * + * That's it! + */ + +struct SmaaInfo { + rt_metrics: vec4, +} + +struct VertexVaryings { + clip_coord: vec2, + tex_coord: vec2, +} + +struct EdgeDetectionVaryings { + @builtin(position) position: vec4, + @location(0) offset_0: vec4, + @location(1) offset_1: vec4, + @location(2) offset_2: vec4, + @location(3) tex_coord: vec2, +} + +struct BlendingWeightCalculationVaryings { + @builtin(position) position: vec4, + @location(0) offset_0: vec4, + @location(1) offset_1: vec4, + @location(2) offset_2: vec4, + @location(3) tex_coord: vec2, +} + +struct NeighborhoodBlendingVaryings { + @builtin(position) position: vec4, + @location(0) offset: vec4, + @location(1) tex_coord: vec2, +} + +@group(0) @binding(0) var color_texture: texture_2d; +@group(0) @binding(1) var smaa_info: SmaaInfo; + +#ifdef SMAA_EDGE_DETECTION +@group(1) @binding(0) var color_sampler: sampler; +#endif // SMAA_EDGE_DETECTION + +#ifdef SMAA_BLENDING_WEIGHT_CALCULATION +@group(1) @binding(0) var edges_texture: texture_2d; +@group(1) @binding(1) var edges_sampler: sampler; +@group(1) @binding(2) var search_texture: texture_2d; +@group(1) @binding(3) var area_texture: texture_2d; +#endif // SMAA_BLENDING_WEIGHT_CALCULATION + +#ifdef SMAA_NEIGHBORHOOD_BLENDING +@group(1) @binding(0) var blend_texture: texture_2d; +@group(1) @binding(1) var blend_sampler: sampler; +#endif // SMAA_NEIGHBORHOOD_BLENDING + +//----------------------------------------------------------------------------- +// SMAA Presets + +#ifdef SMAA_PRESET_LOW +const SMAA_THRESHOLD: f32 = 0.15; +const SMAA_MAX_SEARCH_STEPS: u32 = 4u; +#define SMAA_DISABLE_DIAG_DETECTION +#define SMAA_DISABLE_CORNER_DETECTION +#else ifdef SMAA_PRESET_MEDIUM // SMAA_PRESET_LOW +const SMAA_THRESHOLD: f32 = 0.1; +const SMAA_MAX_SEARCH_STEPS: u32 = 8u; +#define SMAA_DISABLE_DIAG_DETECTION +#define SMAA_DISABLE_CORNER_DETECTION +#else ifdef SMAA_PRESET_HIGH // SMAA_PRESET_MEDIUM +const SMAA_THRESHOLD: f32 = 0.1; +const SMAA_MAX_SEARCH_STEPS: u32 = 16u; +const SMAA_MAX_SEARCH_STEPS_DIAG: u32 = 8u; +const SMAA_CORNER_ROUNDING: u32 = 25u; +#else ifdef SMAA_PRESET_ULTRA // SMAA_PRESET_HIGH +const SMAA_THRESHOLD: f32 = 0.05; +const SMAA_MAX_SEARCH_STEPS: u32 = 32u; +const SMAA_MAX_SEARCH_STEPS_DIAG: u32 = 16u; +const SMAA_CORNER_ROUNDING: u32 = 25u; +#else // SMAA_PRESET_ULTRA +const SMAA_THRESHOLD: f32 = 0.1; +const SMAA_MAX_SEARCH_STEPS: u32 = 16u; +const SMAA_MAX_SEARCH_STEPS_DIAG: u32 = 8u; +const SMAA_CORNER_ROUNDING: u32 = 25u; +#endif // SMAA_PRESET_ULTRA + +//----------------------------------------------------------------------------- +// Configurable Defines + +/** + * SMAA_THRESHOLD specifies the threshold or sensitivity to edges. + * Lowering this value you will be able to detect more edges at the expense of + * performance. + * + * Range: [0, 0.5] + * 0.1 is a reasonable value, and allows to catch most visible edges. + * 0.05 is a rather overkill value, that allows to catch 'em all. + * + * If temporal supersampling is used, 0.2 could be a reasonable value, as low + * contrast edges are properly filtered by just 2x. + */ +// (In the WGSL version of this shader, `SMAA_THRESHOLD` is set above, in "SMAA +// Presets".) + +/** + * SMAA_MAX_SEARCH_STEPS specifies the maximum steps performed in the + * horizontal/vertical pattern searches, at each side of the pixel. + * + * In number of pixels, it's actually the double. So the maximum line length + * perfectly handled by, for example 16, is 64 (by perfectly, we meant that + * longer lines won't look as good, but still antialiased). + * + * Range: [0, 112] + */ +// (In the WGSL version of this shader, `SMAA_MAX_SEARCH_STEPS` is set above, in +// "SMAA Presets".) + +/** + * SMAA_MAX_SEARCH_STEPS_DIAG specifies the maximum steps performed in the + * diagonal pattern searches, at each side of the pixel. In this case we jump + * one pixel at time, instead of two. + * + * Range: [0, 20] + * + * On high-end machines it is cheap (between a 0.8x and 0.9x slower for 16 + * steps), but it can have a significant impact on older machines. + * + * Define SMAA_DISABLE_DIAG_DETECTION to disable diagonal processing. + */ +// (In the WGSL version of this shader, `SMAA_MAX_SEARCH_STEPS_DIAG` is set +// above, in "SMAA Presets".) + +/** + * SMAA_CORNER_ROUNDING specifies how much sharp corners will be rounded. + * + * Range: [0, 100] + * + * Define SMAA_DISABLE_CORNER_DETECTION to disable corner processing. + */ +// (In the WGSL version of this shader, `SMAA_CORNER_ROUNDING` is set above, in +// "SMAA Presets".) + +/** + * If there is an neighbor edge that has SMAA_LOCAL_CONTRAST_FACTOR times + * bigger contrast than current edge, current edge will be discarded. + * + * This allows to eliminate spurious crossing edges, and is based on the fact + * that, if there is too much contrast in a direction, that will hide + * perceptually contrast in the other neighbors. + */ +const SMAA_LOCAL_CONTRAST_ADAPTATION_FACTOR: f32 = 2.0; + +//----------------------------------------------------------------------------- +// Non-Configurable Defines + +const SMAA_AREATEX_MAX_DISTANCE: f32 = 16.0; +const SMAA_AREATEX_MAX_DISTANCE_DIAG: f32 = 20.0; +const SMAA_AREATEX_PIXEL_SIZE: vec2 = (1.0 / vec2(160.0, 560.0)); +const SMAA_AREATEX_SUBTEX_SIZE: f32 = (1.0 / 7.0); +const SMAA_SEARCHTEX_SIZE: vec2 = vec2(66.0, 33.0); +const SMAA_SEARCHTEX_PACKED_SIZE: vec2 = vec2(64.0, 16.0); + +#ifndef SMAA_DISABLE_CORNER_DETECTION +const SMAA_CORNER_ROUNDING_NORM: f32 = f32(SMAA_CORNER_ROUNDING) / 100.0; +#endif // SMAA_DISABLE_CORNER_DETECTION + +//----------------------------------------------------------------------------- +// WGSL-Specific Functions + +// This vertex shader produces the following, when drawn using indices 0..3: +// +// 1 | 0-----x.....2 +// 0 | | s | . ´ +// -1 | x_____x´ +// -2 | : .´ +// -3 | 1´ +// +--------------- +// -1 0 1 2 3 +// +// The axes are clip-space x and y. The region marked s is the visible region. +// The digits in the corners of the right-angled triangle are the vertex +// indices. +// +// The top-left has UV 0,0, the bottom-left has 0,2, and the top-right has 2,0. +// This means that the UV gets interpolated to 1,1 at the bottom-right corner +// of the clip-space rectangle that is at 1,-1 in clip space. +fn calculate_vertex_varyings(vertex_index: u32) -> VertexVaryings { + // See the explanation above for how this works + let uv = vec2(f32(vertex_index >> 1u), f32(vertex_index & 1u)) * 2.0; + let clip_position = vec2(uv * vec2(2.0, -2.0) + vec2(-1.0, 1.0)); + + return VertexVaryings(clip_position, uv); +} + +//----------------------------------------------------------------------------- +// Vertex Shaders + +#ifdef SMAA_EDGE_DETECTION + +/** + * Edge Detection Vertex Shader + */ +@vertex +fn edge_detection_vertex_main(@builtin(vertex_index) vertex_index: u32) -> EdgeDetectionVaryings { + let varyings = calculate_vertex_varyings(vertex_index); + + var edge_detection_varyings = EdgeDetectionVaryings(); + edge_detection_varyings.position = vec4(varyings.clip_coord, 0.0, 1.0); + edge_detection_varyings.tex_coord = varyings.tex_coord; + + edge_detection_varyings.offset_0 = smaa_info.rt_metrics.xyxy * vec4(-1.0, 0.0, 0.0, -1.0) + + varyings.tex_coord.xyxy; + edge_detection_varyings.offset_1 = smaa_info.rt_metrics.xyxy * vec4(1.0, 0.0, 0.0, 1.0) + + varyings.tex_coord.xyxy; + edge_detection_varyings.offset_2 = smaa_info.rt_metrics.xyxy * vec4(-2.0, 0.0, 0.0, -2.0) + + varyings.tex_coord.xyxy; + + return edge_detection_varyings; +} + +#endif // SMAA_EDGE_DETECTION + +#ifdef SMAA_BLENDING_WEIGHT_CALCULATION + +/** + * Blend Weight Calculation Vertex Shader + */ +@vertex +fn blending_weight_calculation_vertex_main(@builtin(vertex_index) vertex_index: u32) + -> BlendingWeightCalculationVaryings { + let varyings = calculate_vertex_varyings(vertex_index); + + var weight_varyings = BlendingWeightCalculationVaryings(); + weight_varyings.position = vec4(varyings.clip_coord, 0.0, 1.0); + weight_varyings.tex_coord = varyings.tex_coord; + + // We will use these offsets for the searches later on (see @PSEUDO_GATHER4): + weight_varyings.offset_0 = smaa_info.rt_metrics.xyxy * vec4(-0.25, -0.125, 1.25, -0.125) + + varyings.tex_coord.xyxy; + weight_varyings.offset_1 = smaa_info.rt_metrics.xyxy * vec4(-0.125, -0.25, -0.125, 1.25) + + varyings.tex_coord.xyxy; + + // And these for the searches, they indicate the ends of the loops: + weight_varyings.offset_2 = + smaa_info.rt_metrics.xxyy * vec4(-2.0, 2.0, -2.0, 2.0) * f32(SMAA_MAX_SEARCH_STEPS) + + vec4(weight_varyings.offset_0.xz, weight_varyings.offset_1.yw); + + return weight_varyings; +} + +#endif // SMAA_BLENDING_WEIGHT_CALCULATION + +#ifdef SMAA_NEIGHBORHOOD_BLENDING + +/** + * Neighborhood Blending Vertex Shader + */ +@vertex +fn neighborhood_blending_vertex_main(@builtin(vertex_index) vertex_index: u32) + -> NeighborhoodBlendingVaryings { + let varyings = calculate_vertex_varyings(vertex_index); + let offset = smaa_info.rt_metrics.xyxy * vec4(1.0, 0.0, 0.0, 1.0) + varyings.tex_coord.xyxy; + return NeighborhoodBlendingVaryings( + vec4(varyings.clip_coord, 0.0, 1.0), + offset, + varyings.tex_coord + ); +} + +#endif // SMAA_NEIGHBORHOOD_BLENDING + +//----------------------------------------------------------------------------- +// Edge Detection Pixel Shaders (First Pass) + +#ifdef SMAA_EDGE_DETECTION + +/** + * Luma Edge Detection + * + * IMPORTANT NOTICE: luma edge detection requires gamma-corrected colors, and + * thus 'color_texture' should be a non-sRGB texture. + */ +@fragment +fn luma_edge_detection_fragment_main(in: EdgeDetectionVaryings) -> @location(0) vec4 { + // Calculate the threshold: + // TODO: Predication. + let threshold = vec2(SMAA_THRESHOLD); + + // Calculate luma: + let weights = vec3(0.2126, 0.7152, 0.0722); + let L = dot(textureSample(color_texture, color_sampler, in.tex_coord).rgb, weights); + + let Lleft = dot(textureSample(color_texture, color_sampler, in.offset_0.xy).rgb, weights); + let Ltop = dot(textureSample(color_texture, color_sampler, in.offset_0.zw).rgb, weights); + + // We do the usual threshold: + var delta: vec4 = vec4(abs(L - vec2(Lleft, Ltop)), 0.0, 0.0); + var edges = step(threshold, delta.xy); + + // Then discard if there is no edge: + if (dot(edges, vec2(1.0)) == 0.0) { + discard; + } + + // Calculate right and bottom deltas: + let Lright = dot(textureSample(color_texture, color_sampler, in.offset_1.xy).rgb, weights); + let Lbottom = dot(textureSample(color_texture, color_sampler, in.offset_1.zw).rgb, weights); + delta = vec4(delta.xy, abs(L - vec2(Lright, Lbottom))); + + // Calculate the maximum delta in the direct neighborhood: + var max_delta = max(delta.xy, delta.zw); + + // Calculate left-left and top-top deltas: + let Lleftleft = dot(textureSample(color_texture, color_sampler, in.offset_2.xy).rgb, weights); + let Ltoptop = dot(textureSample(color_texture, color_sampler, in.offset_2.zw).rgb, weights); + delta = vec4(delta.xy, abs(vec2(Lleft, Ltop) - vec2(Lleftleft, Ltoptop))); + + // Calculate the final maximum delta: + max_delta = max(max_delta.xy, delta.zw); + let final_delta = max(max_delta.x, max_delta.y); + + // Local contrast adaptation: + edges *= step(vec2(final_delta), SMAA_LOCAL_CONTRAST_ADAPTATION_FACTOR * delta.xy); + + return vec4(edges, 0.0, 1.0); +} + +#endif // SMAA_EDGE_DETECTION + +#ifdef SMAA_BLENDING_WEIGHT_CALCULATION + +//----------------------------------------------------------------------------- +// Diagonal Search Functions + +#ifndef SMAA_DISABLE_DIAG_DETECTION + +/** + * Allows to decode two binary values from a bilinear-filtered access. + */ +fn decode_diag_bilinear_access_2(in_e: vec2) -> vec2 { + // Bilinear access for fetching 'e' have a 0.25 offset, and we are + // interested in the R and G edges: + // + // +---G---+-------+ + // | x o R x | + // +-------+-------+ + // + // Then, if one of these edge is enabled: + // Red: (0.75 * X + 0.25 * 1) => 0.25 or 1.0 + // Green: (0.75 * 1 + 0.25 * X) => 0.75 or 1.0 + // + // This function will unpack the values (mad + mul + round): + // wolframalpha.com: round(x * abs(5 * x - 5 * 0.75)) plot 0 to 1 + var e = in_e; + e.r = e.r * abs(5.0 * e.r - 5.0 * 0.75); + return round(e); +} + +fn decode_diag_bilinear_access_4(e: vec4) -> vec4 { + let e_rb = e.rb * abs(5.0 * e.rb - 5.0 * 0.75); + return round(vec4(e_rb.x, e.g, e_rb.y, e.a)); +} + +/** + * These functions allows to perform diagonal pattern searches. + */ +fn search_diag_1(tex_coord: vec2, dir: vec2, e: ptr>) -> vec2 { + var coord = vec4(tex_coord, -1.0, 1.0); + let t = vec3(smaa_info.rt_metrics.xy, 1.0); + while (coord.z < f32(SMAA_MAX_SEARCH_STEPS_DIAG - 1u) && coord.w > 0.9) { + coord = vec4(t * vec3(dir, 1.0) + coord.xyz, coord.w); + *e = textureSampleLevel(edges_texture, edges_sampler, coord.xy, 0.0).rg; + coord.w = dot(*e, vec2(0.5)); + } + return coord.zw; +} + +fn search_diag_2(tex_coord: vec2, dir: vec2, e: ptr>) -> vec2 { + var coord = vec4(tex_coord, -1.0, 1.0); + coord.x += 0.25 * smaa_info.rt_metrics.x; // See @SearchDiag2Optimization + let t = vec3(smaa_info.rt_metrics.xy, 1.0); + while (coord.z < f32(SMAA_MAX_SEARCH_STEPS_DIAG - 1u) && coord.w > 0.9) { + coord = vec4(t * vec3(dir, 1.0) + coord.xyz, coord.w); + + // @SearchDiag2Optimization + // Fetch both edges at once using bilinear filtering: + *e = textureSampleLevel(edges_texture, edges_sampler, coord.xy, 0.0).rg; + *e = decode_diag_bilinear_access_2(*e); + + // Non-optimized version: + // e.g = SMAASampleLevelZero(edgesTex, coord.xy).g; + // e.r = SMAASampleLevelZeroOffset(edgesTex, coord.xy, int2(1, 0)).r; + + coord.w = dot(*e, vec2(0.5)); + } + return coord.zw; +} + +/** + * Similar to SMAAArea, this calculates the area corresponding to a certain + * diagonal distance and crossing edges 'e'. + */ +fn area_diag(dist: vec2, e: vec2, offset: f32) -> vec2 { + var tex_coord = vec2(SMAA_AREATEX_MAX_DISTANCE_DIAG) * e + dist; + + // We do a scale and bias for mapping to texel space: + tex_coord = SMAA_AREATEX_PIXEL_SIZE * tex_coord + 0.5 * SMAA_AREATEX_PIXEL_SIZE; + + // Diagonal areas are on the second half of the texture: + tex_coord.x += 0.5; + + // Move to proper place, according to the subpixel offset: + tex_coord.y += SMAA_AREATEX_SUBTEX_SIZE * offset; + + // Do it! + return textureSampleLevel(area_texture, edges_sampler, tex_coord, 0.0).rg; +} + +/** + * This searches for diagonal patterns and returns the corresponding weights. + */ +fn calculate_diag_weights(tex_coord: vec2, e: vec2, subsample_indices: vec4) + -> vec2 { + var weights = vec2(0.0, 0.0); + + // Search for the line ends: + var d = vec4(0.0); + var end = vec2(0.0); + if (e.r > 0.0) { + let d_xz = search_diag_1(tex_coord, vec2(-1.0, 1.0), &end); + d = vec4(d_xz.x, d.y, d_xz.y, d.w); + d.x += f32(end.y > 0.9); + } else { + d = vec4(0.0, d.y, 0.0, d.w); + } + let d_yw = search_diag_1(tex_coord, vec2(1.0, -1.0), &end); + d = vec4(d.x, d_yw.x, d.y, d_yw.y); + + if (d.x + d.y > 2.0) { // d.x + d.y + 1 > 3 + // Fetch the crossing edges: + let coords = vec4(-d.x + 0.25, d.x, d.y, -d.y - 0.25) * smaa_info.rt_metrics.xyxy + + tex_coord.xyxy; + var c = vec4( + textureSampleLevel(edges_texture, edges_sampler, coords.xy, 0.0, vec2(-1, 0)).rg, + textureSampleLevel(edges_texture, edges_sampler, coords.zw, 0.0, vec2( 1, 0)).rg, + ); + let c_yxwz = decode_diag_bilinear_access_4(c.xyzw); + c = c_yxwz.yxwz; + + // Non-optimized version: + // float4 coords = mad(float4(-d.x, d.x, d.y, -d.y), SMAA_RT_METRICS.xyxy, texcoord.xyxy); + // float4 c; + // c.x = SMAASampleLevelZeroOffset(edgesTex, coords.xy, int2(-1, 0)).g; + // c.y = SMAASampleLevelZeroOffset(edgesTex, coords.xy, int2( 0, 0)).r; + // c.z = SMAASampleLevelZeroOffset(edgesTex, coords.zw, int2( 1, 0)).g; + // c.w = SMAASampleLevelZeroOffset(edgesTex, coords.zw, int2( 1, -1)).r; + + // Merge crossing edges at each side into a single value: + var cc = vec2(2.0) * c.xz + c.yw; + + // Remove the crossing edge if we didn't found the end of the line: + cc = select(cc, vec2(0.0, 0.0), vec2(step(vec2(0.9), d.zw))); + + // Fetch the areas for this line: + weights += area_diag(d.xy, cc, subsample_indices.z); + } + + // Search for the line ends: + let d_xz = search_diag_2(tex_coord, vec2(-1.0, -1.0), &end); + if (textureSampleLevel(edges_texture, edges_sampler, tex_coord, 0.0, vec2(1, 0)).r > 0.0) { + let d_yw = search_diag_2(tex_coord, vec2(1.0, 1.0), &end); + d = vec4(d.x, d_yw.x, d.z, d_yw.y); + d.y += f32(end.y > 0.9); + } else { + d = vec4(d.x, 0.0, d.z, 0.0); + } + + if (d.x + d.y > 2.0) { // d.x + d.y + 1 > 3 + // Fetch the crossing edges: + let coords = vec4(-d.x, -d.x, d.y, d.y) * smaa_info.rt_metrics.xyxy + tex_coord.xyxy; + let c = vec4( + textureSampleLevel(edges_texture, edges_sampler, coords.xy, 0.0, vec2(-1, 0)).g, + textureSampleLevel(edges_texture, edges_sampler, coords.xy, 0.0, vec2( 0, -1)).r, + textureSampleLevel(edges_texture, edges_sampler, coords.zw, 0.0, vec2( 1, 0)).gr, + ); + var cc = vec2(2.0) * c.xz + c.yw; + + // Remove the crossing edge if we didn't found the end of the line: + cc = select(cc, vec2(0.0, 0.0), vec2(step(vec2(0.9), d.zw))); + + // Fetch the areas for this line: + weights += area_diag(d.xy, cc, subsample_indices.w).gr; + } + + return weights; +} + +#endif // SMAA_DISABLE_DIAG_DETECTION + +//----------------------------------------------------------------------------- +// Horizontal/Vertical Search Functions + +/** + * This allows to determine how much length should we add in the last step + * of the searches. It takes the bilinearly interpolated edge (see + * @PSEUDO_GATHER4), and adds 0, 1 or 2, depending on which edges and + * crossing edges are active. + */ +fn search_length(e: vec2, offset: f32) -> f32 { + // The texture is flipped vertically, with left and right cases taking half + // of the space horizontally: + var scale = SMAA_SEARCHTEX_SIZE * vec2(0.5, -1.0); + var bias = SMAA_SEARCHTEX_SIZE * vec2(offset, 1.0); + + // Scale and bias to access texel centers: + scale += vec2(-1.0, 1.0); + bias += vec2( 0.5, -0.5); + + // Convert from pixel coordinates to texcoords: + // (We use SMAA_SEARCHTEX_PACKED_SIZE because the texture is cropped) + scale *= 1.0 / SMAA_SEARCHTEX_PACKED_SIZE; + bias *= 1.0 / SMAA_SEARCHTEX_PACKED_SIZE; + + // Lookup the search texture: + return textureSampleLevel(search_texture, edges_sampler, scale * e + bias, 0.0).r; +} + +/** + * Horizontal/vertical search functions for the 2nd pass. + */ +fn search_x_left(in_tex_coord: vec2, end: f32) -> f32 { + var tex_coord = in_tex_coord; + + /** + * @PSEUDO_GATHER4 + * This texcoord has been offset by (-0.25, -0.125) in the vertex shader to + * sample between edge, thus fetching four edges in a row. + * Sampling with different offsets in each direction allows to disambiguate + * which edges are active from the four fetched ones. + */ + var e = vec2(0.0, 1.0); + while (tex_coord.x > end && + e.g > 0.8281 && // Is there some edge not activated? + e.r == 0.0) { // Or is there a crossing edge that breaks the line? + e = textureSampleLevel(edges_texture, edges_sampler, tex_coord, 0.0).rg; + tex_coord += -vec2(2.0, 0.0) * smaa_info.rt_metrics.xy; + } + let offset = -(255.0 / 127.0) * search_length(e, 0.0) + 3.25; + return smaa_info.rt_metrics.x * offset + tex_coord.x; +} + +fn search_x_right(in_tex_coord: vec2, end: f32) -> f32 { + var tex_coord = in_tex_coord; + + var e = vec2(0.0, 1.0); + while (tex_coord.x < end && + e.g > 0.8281 && // Is there some edge not activated? + e.r == 0.0) { // Or is there a crossing edge that breaks the line? + e = textureSampleLevel(edges_texture, edges_sampler, tex_coord, 0.0).rg; + tex_coord += vec2(2.0, 0.0) * smaa_info.rt_metrics.xy; + } + let offset = -(255.0 / 127.0) * search_length(e, 0.5) + 3.25; + return -smaa_info.rt_metrics.x * offset + tex_coord.x; +} + +fn search_y_up(in_tex_coord: vec2, end: f32) -> f32 { + var tex_coord = in_tex_coord; + + var e = vec2(1.0, 0.0); + while (tex_coord.y > end && + e.r > 0.8281 && // Is there some edge not activated? + e.g == 0.0) { // Or is there a crossing edge that breaks the line? + e = textureSampleLevel(edges_texture, edges_sampler, tex_coord, 0.0).rg; + tex_coord += -vec2(0.0, 2.0) * smaa_info.rt_metrics.xy; + } + let offset = -(255.0 / 127.0) * search_length(e.gr, 0.0) + 3.25; + return smaa_info.rt_metrics.y * offset + tex_coord.y; +} + +fn search_y_down(in_tex_coord: vec2, end: f32) -> f32 { + var tex_coord = in_tex_coord; + + var e = vec2(1.0, 0.0); + while (tex_coord.y < end && + e.r > 0.8281 && // Is there some edge not activated? + e.g == 0.0) { // Or is there a crossing edge that breaks the line? + e = textureSampleLevel(edges_texture, edges_sampler, tex_coord, 0.0).rg; + tex_coord += vec2(0.0, 2.0) * smaa_info.rt_metrics.xy; + } + let offset = -(255.0 / 127.0) * search_length(e.gr, 0.5) + 3.25; + return -smaa_info.rt_metrics.y * offset + tex_coord.y; +} + +/** + * Ok, we have the distance and both crossing edges. So, what are the areas + * at each side of current edge? + */ +fn area(dist: vec2, e1: f32, e2: f32, offset: f32) -> vec2 { + // Rounding prevents precision errors of bilinear filtering: + var tex_coord = SMAA_AREATEX_MAX_DISTANCE * round(4.0 * vec2(e1, e2)) + dist; + + // We do a scale and bias for mapping to texel space: + tex_coord = SMAA_AREATEX_PIXEL_SIZE * tex_coord + 0.5 * SMAA_AREATEX_PIXEL_SIZE; + + // Move to proper place, according to the subpixel offset: + tex_coord.y += SMAA_AREATEX_SUBTEX_SIZE * offset; + + // Do it! + return textureSample(area_texture, edges_sampler, tex_coord).rg; +} + +//----------------------------------------------------------------------------- +// Corner Detection Functions + +fn detect_horizontal_corner_pattern(weights: vec2, tex_coord: vec4, d: vec2) + -> vec2 { +#ifndef SMAA_DISABLE_CORNER_DETECTION + let left_right = step(d.xy, d.yx); + var rounding = (1.0 - SMAA_CORNER_ROUNDING_NORM) * left_right; + + rounding /= left_right.x + left_right.y; // Reduce blending for pixels in the center of a line. + + var factor = vec2(1.0, 1.0); + factor.x -= rounding.x * + textureSampleLevel(edges_texture, edges_sampler, tex_coord.xy, 0.0, vec2(0, 1)).r; + factor.x -= rounding.y * + textureSampleLevel(edges_texture, edges_sampler, tex_coord.zw, 0.0, vec2(1, 1)).r; + factor.y -= rounding.x * + textureSampleLevel(edges_texture, edges_sampler, tex_coord.xy, 0.0, vec2(0, -2)).r; + factor.y -= rounding.y * + textureSampleLevel(edges_texture, edges_sampler, tex_coord.zw, 0.0, vec2(1, -2)).r; + + return weights * saturate(factor); +#else // SMAA_DISABLE_CORNER_DETECTION + return weights; +#endif // SMAA_DISABLE_CORNER_DETECTION +} + +fn detect_vertical_corner_pattern(weights: vec2, tex_coord: vec4, d: vec2) + -> vec2 { +#ifndef SMAA_DISABLE_CORNER_DETECTION + let left_right = step(d.xy, d.yx); + var rounding = (1.0 - SMAA_CORNER_ROUNDING_NORM) * left_right; + + rounding /= left_right.x + left_right.y; + + var factor = vec2(1.0, 1.0); + factor.x -= rounding.x * + textureSampleLevel(edges_texture, edges_sampler, tex_coord.xy, 0.0, vec2( 1, 0)).g; + factor.x -= rounding.y * + textureSampleLevel(edges_texture, edges_sampler, tex_coord.zw, 0.0, vec2( 1, 1)).g; + factor.y -= rounding.x * + textureSampleLevel(edges_texture, edges_sampler, tex_coord.xy, 0.0, vec2(-2, 0)).g; + factor.y -= rounding.y * + textureSampleLevel(edges_texture, edges_sampler, tex_coord.zw, 0.0, vec2(-2, 1)).g; + + return weights * saturate(factor); +#else // SMAA_DISABLE_CORNER_DETECTION + return weights; +#endif // SMAA_DISABLE_CORNER_DETECTION +} + +//----------------------------------------------------------------------------- +// Blending Weight Calculation Pixel Shader (Second Pass) + +@fragment +fn blending_weight_calculation_fragment_main(in: BlendingWeightCalculationVaryings) + -> @location(0) vec4 { + let subsample_indices = vec4(0.0); // Just pass zero for SMAA 1x, see @SUBSAMPLE_INDICES. + + var weights = vec4(0.0); + + var e = textureSample(edges_texture, edges_sampler, in.tex_coord).rg; + + if (e.g > 0.0) { // Edge at north +#ifndef SMAA_DISABLE_DIAG_DETECTION + // Diagonals have both north and west edges, so searching for them in + // one of the boundaries is enough. + weights = vec4(calculate_diag_weights(in.tex_coord, e, subsample_indices), weights.ba); + + // We give priority to diagonals, so if we find a diagonal we skip + // horizontal/vertical processing. + if (weights.r + weights.g != 0.0) { + return weights; + } +#endif // SMAA_DISABLE_DIAG_DETECTION + + var d: vec2; + + // Find the distance to the left: + var coords: vec3; + coords.x = search_x_left(in.offset_0.xy, in.offset_2.x); + // in.offset_1.y = in.tex_coord.y - 0.25 * smaa_info.rt_metrics.y (@CROSSING_OFFSET) + coords.y = in.offset_1.y; + d.x = coords.x; + + // Now fetch the left crossing edges, two at a time using bilinear + // filtering. Sampling at -0.25 (see @CROSSING_OFFSET) enables to + // discern what value each edge has: + let e1 = textureSampleLevel(edges_texture, edges_sampler, coords.xy, 0.0).r; + + // Find the distance to the right: + coords.z = search_x_right(in.offset_0.zw, in.offset_2.y); + d.y = coords.z; + + // We want the distances to be in pixel units (doing this here allow to + // better interleave arithmetic and memory accesses): + d = abs(round(smaa_info.rt_metrics.zz * d - in.position.xx)); + + // SMAAArea below needs a sqrt, as the areas texture is compressed + // quadratically: + let sqrt_d = sqrt(d); + + // Fetch the right crossing edges: + let e2 = textureSampleLevel( + edges_texture, edges_sampler, coords.zy, 0.0, vec2(1, 0)).r; + + // Ok, we know how this pattern looks like, now it is time for getting + // the actual area: + weights = vec4(area(sqrt_d, e1, e2, subsample_indices.y), weights.ba); + + // Fix corners: + coords.y = in.tex_coord.y; + weights = vec4( + detect_horizontal_corner_pattern(weights.rg, coords.xyzy, d), + weights.ba + ); + } + + if (e.r > 0.0) { // Edge at west + var d: vec2; + + // Find the distance to the top: + var coords: vec3; + coords.y = search_y_up(in.offset_1.xy, in.offset_2.z); + // in.offset_1.x = in.tex_coord.x - 0.25 * smaa_info.rt_metrics.x + coords.x = in.offset_0.x; + d.x = coords.y; + + // Fetch the top crossing edges: + let e1 = textureSampleLevel(edges_texture, edges_sampler, coords.xy, 0.0).g; + + // Find the distance to the bottom: + coords.z = search_y_down(in.offset_1.zw, in.offset_2.w); + d.y = coords.z; + + // We want the distances to be in pixel units: + d = abs(round(smaa_info.rt_metrics.ww * d - in.position.yy)); + + // SMAAArea below needs a sqrt, as the areas texture is compressed + // quadratically: + let sqrt_d = sqrt(d); + + // Fetch the bottom crossing edges: + let e2 = textureSampleLevel( + edges_texture, edges_sampler, coords.xz, 0.0, vec2(0, 1)).g; + + // Get the area for this direction: + weights = vec4(weights.rg, area(sqrt_d, e1, e2, subsample_indices.x)); + + // Fix corners: + coords.x = in.tex_coord.x; + weights = vec4(weights.rg, detect_vertical_corner_pattern(weights.ba, coords.xyxz, d)); + } + + return weights; +} + +#endif // SMAA_BLENDING_WEIGHT_CALCULATION + +#ifdef SMAA_NEIGHBORHOOD_BLENDING + +//----------------------------------------------------------------------------- +// Neighborhood Blending Pixel Shader (Third Pass) + +@fragment +fn neighborhood_blending_fragment_main(in: NeighborhoodBlendingVaryings) -> @location(0) vec4 { + // Fetch the blending weights for current pixel: + let a = vec4( + textureSample(blend_texture, blend_sampler, in.offset.xy).a, // Right + textureSample(blend_texture, blend_sampler, in.offset.zw).g, // Top + textureSample(blend_texture, blend_sampler, in.tex_coord).zx, // Bottom / Left + ); + + // Is there any blending weight with a value greater than 0.0? + if (dot(a, vec4(1.0)) < 1.0e-5) { + let color = textureSampleLevel(color_texture, blend_sampler, in.tex_coord, 0.0); + // TODO: Reprojection + return color; + } else { + let h = max(a.x, a.z) > max(a.y, a.w); // max(horizontal) > max(vertical) + + // Calculate the blending offsets: + var blending_offset = vec4(0.0, a.y, 0.0, a.w); + var blending_weight = a.yw; + blending_offset = select(blending_offset, vec4(a.x, 0.0, a.z, 0.0), h); + blending_weight = select(blending_weight, a.xz, h); + blending_weight /= dot(blending_weight, vec2(1.0)); + + // Calculate the texture coordinates: + let blending_coord = + blending_offset * vec4(smaa_info.rt_metrics.xy, -smaa_info.rt_metrics.xy) + + in.tex_coord.xyxy; + + // We exploit bilinear filtering to mix current pixel with the chosen + // neighbor: + var color = blending_weight.x * + textureSampleLevel(color_texture, blend_sampler, blending_coord.xy, 0.0); + color += blending_weight.y * + textureSampleLevel(color_texture, blend_sampler, blending_coord.zw, 0.0); + + // TODO: Reprojection + + return color; + } +} + +#endif // SMAA_NEIGHBORHOOD_BLENDING diff --git a/examples/3d/anti_aliasing.rs b/examples/3d/anti_aliasing.rs index 81c2156a55..7b04a499a0 100644 --- a/examples/3d/anti_aliasing.rs +++ b/examples/3d/anti_aliasing.rs @@ -1,6 +1,7 @@ //! This example compares MSAA (Multi-Sample Anti-aliasing), FXAA (Fast Approximate Anti-aliasing), and TAA (Temporal Anti-aliasing). use std::f32::consts::PI; +use std::fmt::Write; use bevy::{ core_pipeline::{ @@ -9,6 +10,7 @@ use bevy::{ TemporalAntiAliasBundle, TemporalAntiAliasPlugin, TemporalAntiAliasSettings, }, fxaa::{Fxaa, Sensitivity}, + smaa::{SmaaPreset, SmaaSettings}, }, pbr::CascadeShadowConfigBuilder, prelude::*, @@ -34,6 +36,7 @@ fn modify_aa( ( Entity, Option<&mut Fxaa>, + Option<&mut SmaaSettings>, Option<&TemporalAntiAliasSettings>, ), With, @@ -41,19 +44,21 @@ fn modify_aa( mut msaa: ResMut, mut commands: Commands, ) { - let (camera_entity, fxaa, taa) = camera.single_mut(); + let (camera_entity, fxaa, smaa, taa) = camera.single_mut(); let mut camera = commands.entity(camera_entity); // No AA if keys.just_pressed(KeyCode::Digit1) { *msaa = Msaa::Off; camera.remove::(); + camera.remove::(); camera.remove::(); } // MSAA if keys.just_pressed(KeyCode::Digit2) && *msaa == Msaa::Off { camera.remove::(); + camera.remove::(); camera.remove::(); *msaa = Msaa::Sample4; @@ -75,6 +80,7 @@ fn modify_aa( // FXAA if keys.just_pressed(KeyCode::Digit3) && fxaa.is_none() { *msaa = Msaa::Off; + camera.remove::(); camera.remove::(); camera.insert(Fxaa::default()); @@ -104,10 +110,36 @@ fn modify_aa( } } - // TAA - if keys.just_pressed(KeyCode::Digit4) && taa.is_none() { + // SMAA + if keys.just_pressed(KeyCode::Digit4) && smaa.is_none() { *msaa = Msaa::Off; camera.remove::(); + camera.remove::(); + + camera.insert(SmaaSettings::default()); + } + + // SMAA Settings + if let Some(mut smaa) = smaa { + if keys.just_pressed(KeyCode::KeyQ) { + smaa.preset = SmaaPreset::Low; + } + if keys.just_pressed(KeyCode::KeyW) { + smaa.preset = SmaaPreset::Medium; + } + if keys.just_pressed(KeyCode::KeyE) { + smaa.preset = SmaaPreset::High; + } + if keys.just_pressed(KeyCode::KeyR) { + smaa.preset = SmaaPreset::Ultra; + } + } + + // TAA + if keys.just_pressed(KeyCode::Digit5) && taa.is_none() { + *msaa = Msaa::Off; + camera.remove::(); + camera.remove::(); camera.insert(TemporalAntiAliasBundle::default()); } @@ -141,6 +173,7 @@ fn update_ui( camera: Query< ( Option<&Fxaa>, + Option<&SmaaSettings>, Option<&TemporalAntiAliasSettings>, &ContrastAdaptiveSharpeningSettings, ), @@ -149,104 +182,67 @@ fn update_ui( msaa: Res, mut ui: Query<&mut Text>, ) { - let (fxaa, taa, cas_settings) = camera.single(); + let (fxaa, smaa, taa, cas_settings) = camera.single(); let mut ui = ui.single_mut(); let ui = &mut ui.sections[0].value; *ui = "Antialias Method\n".to_string(); - if *msaa == Msaa::Off && fxaa.is_none() && taa.is_none() { - ui.push_str("(1) *No AA*\n"); - } else { - ui.push_str("(1) No AA\n"); - } + draw_selectable_menu_item( + ui, + "No AA", + '1', + *msaa == Msaa::Off && fxaa.is_none() && taa.is_none() && smaa.is_none(), + ); + draw_selectable_menu_item(ui, "MSAA", '2', *msaa != Msaa::Off); + draw_selectable_menu_item(ui, "FXAA", '3', fxaa.is_some()); + draw_selectable_menu_item(ui, "SMAA", '4', smaa.is_some()); + draw_selectable_menu_item(ui, "TAA", '5', taa.is_some()); if *msaa != Msaa::Off { - ui.push_str("(2) *MSAA*\n"); - } else { - ui.push_str("(2) MSAA\n"); - } - - if fxaa.is_some() { - ui.push_str("(3) *FXAA*\n"); - } else { - ui.push_str("(3) FXAA\n"); - } - - if taa.is_some() { - ui.push_str("(4) *TAA*"); - } else { - ui.push_str("(4) TAA"); - } - - if *msaa != Msaa::Off { - ui.push_str("\n\n----------\n\nSample Count\n"); - - if *msaa == Msaa::Sample2 { - ui.push_str("(Q) *2*\n"); - } else { - ui.push_str("(Q) 2\n"); - } - if *msaa == Msaa::Sample4 { - ui.push_str("(W) *4*\n"); - } else { - ui.push_str("(W) 4\n"); - } - if *msaa == Msaa::Sample8 { - ui.push_str("(E) *8*"); - } else { - ui.push_str("(E) 8"); - } + ui.push_str("\n----------\n\nSample Count\n"); + draw_selectable_menu_item(ui, "2", 'Q', *msaa == Msaa::Sample2); + draw_selectable_menu_item(ui, "4", 'W', *msaa == Msaa::Sample4); + draw_selectable_menu_item(ui, "8", 'E', *msaa == Msaa::Sample8); } if let Some(fxaa) = fxaa { - ui.push_str("\n\n----------\n\nSensitivity\n"); - - if fxaa.edge_threshold == Sensitivity::Low { - ui.push_str("(Q) *Low*\n"); - } else { - ui.push_str("(Q) Low\n"); - } - - if fxaa.edge_threshold == Sensitivity::Medium { - ui.push_str("(W) *Medium*\n"); - } else { - ui.push_str("(W) Medium\n"); - } - - if fxaa.edge_threshold == Sensitivity::High { - ui.push_str("(E) *High*\n"); - } else { - ui.push_str("(E) High\n"); - } - - if fxaa.edge_threshold == Sensitivity::Ultra { - ui.push_str("(R) *Ultra*\n"); - } else { - ui.push_str("(R) Ultra\n"); - } - - if fxaa.edge_threshold == Sensitivity::Extreme { - ui.push_str("(T) *Extreme*"); - } else { - ui.push_str("(T) Extreme"); - } + ui.push_str("\n----------\n\nSensitivity\n"); + draw_selectable_menu_item(ui, "Low", 'Q', fxaa.edge_threshold == Sensitivity::Low); + draw_selectable_menu_item( + ui, + "Medium", + 'W', + fxaa.edge_threshold == Sensitivity::Medium, + ); + draw_selectable_menu_item(ui, "High", 'E', fxaa.edge_threshold == Sensitivity::High); + draw_selectable_menu_item(ui, "Ultra", 'R', fxaa.edge_threshold == Sensitivity::Ultra); + draw_selectable_menu_item( + ui, + "Extreme", + 'T', + fxaa.edge_threshold == Sensitivity::Extreme, + ); } + if let Some(smaa) = smaa { + ui.push_str("\n----------\n\nQuality\n"); + draw_selectable_menu_item(ui, "Low", 'Q', smaa.preset == SmaaPreset::Low); + draw_selectable_menu_item(ui, "Medium", 'W', smaa.preset == SmaaPreset::Medium); + draw_selectable_menu_item(ui, "High", 'E', smaa.preset == SmaaPreset::High); + draw_selectable_menu_item(ui, "Ultra", 'R', smaa.preset == SmaaPreset::Ultra); + } + + ui.push_str("\n----------\n\n"); + draw_selectable_menu_item(ui, "Sharpening", '0', cas_settings.enabled); + 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"); + draw_selectable_menu_item(ui, "Denoising", 'D', cas_settings.denoise); } } @@ -350,6 +346,12 @@ fn setup( ); } +/// Writes a simple menu item that can be on or off. +fn draw_selectable_menu_item(ui: &mut String, label: &str, shortcut: char, enabled: bool) { + let star = if enabled { "*" } else { "" }; + let _ = writeln!(*ui, "({}) {}{}{}", shortcut, star, label, star); +} + /// Creates a colorful test pattern fn uv_debug_texture() -> Image { const TEXTURE_SIZE: usize = 8; diff --git a/typos.toml b/typos.toml index ce87fc5f04..bf338fb5c9 100644 --- a/typos.toml +++ b/typos.toml @@ -12,11 +12,13 @@ extend-exclude = [ # Match Whole Word - Case Sensitive [default.extend-identifiers] -iy = "iy" # Variable name used in bevy_gizmos. Probably stands for "y-axis index", as it's being used in loops. -ser = "ser" # ron::ser - Serializer -SME = "SME" # Subject Matter Expert -Sur = "Sur" # macOS Big Sur - South -Ba = "Ba" # Bitangent for Anisotropy +iy = "iy" # Variable name used in bevy_gizmos. Probably stands for "y-axis index", as it's being used in loops. +ser = "ser" # ron::ser - Serializer +SME = "SME" # Subject Matter Expert +Sur = "Sur" # macOS Big Sur - South +Masia = "Masia" # The surname of one of the authors of SMAA +Ba = "Ba" # Bitangent for Anisotropy +ba = "ba" # Part of an accessor in WGSL - color.ba # Match Inside a Word - Case Insensitive [default.extend-words]