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 <alice.i.cecile@gmail.com>
This commit is contained in:
Patrick Walton 2024-06-04 10:07:34 -07:00 committed by GitHub
parent d0e1b43402
commit ace4c6023b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 2273 additions and 88 deletions

View file

@ -20,6 +20,7 @@ pub mod graph {
Bloom,
Tonemapping,
Fxaa,
Smaa,
Upscaling,
ContrastAdaptiveSharpening,
EndMainPassPostProcessing,

View file

@ -32,6 +32,7 @@ pub mod graph {
DepthOfField,
Tonemapping,
Fxaa,
Smaa,
Upscaling,
ContrastAdaptiveSharpening,
EndMainPassPostProcessing,

View file

@ -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,
));
}
}

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -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<Camera>,
@ -41,19 +44,21 @@ fn modify_aa(
mut msaa: ResMut<Msaa>,
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::<Fxaa>();
camera.remove::<SmaaSettings>();
camera.remove::<TemporalAntiAliasBundle>();
}
// MSAA
if keys.just_pressed(KeyCode::Digit2) && *msaa == Msaa::Off {
camera.remove::<Fxaa>();
camera.remove::<SmaaSettings>();
camera.remove::<TemporalAntiAliasBundle>();
*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::<SmaaSettings>();
camera.remove::<TemporalAntiAliasBundle>();
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::<Fxaa>();
camera.remove::<TemporalAntiAliasBundle>();
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::<Fxaa>();
camera.remove::<SmaaSettings>();
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<Msaa>,
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;

View file

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