From 5640ec855e3e932a4addaa5e5f3d0dd4800f44f8 Mon Sep 17 00:00:00 2001 From: Griffin Date: Wed, 2 Nov 2022 06:51:28 +0000 Subject: [PATCH] Add FXAA postprocessing (#6393) # Objective - Add post processing passes for FXAA (Fast Approximate Anti-Aliasing) - Add example comparing MSAA and FXAA ## Solution When the FXAA plugin is added, passes for FXAA are inserted between the main pass and the tonemapping pass. Supports using either HDR or LDR output from the main pass. --- ## Changelog - Add a new FXAANode that runs after the main pass when the FXAA plugin is added. Co-authored-by: Carter Anderson --- Cargo.toml | 10 + crates/bevy_core_pipeline/src/fxaa/fxaa.wgsl | 277 +++++++++++++++++++ crates/bevy_core_pipeline/src/fxaa/mod.rs | 249 +++++++++++++++++ crates/bevy_core_pipeline/src/fxaa/node.rs | 132 +++++++++ crates/bevy_core_pipeline/src/lib.rs | 5 +- crates/bevy_pbr/src/render/pbr.wgsl | 6 +- examples/3d/fxaa.rs | 187 +++++++++++++ examples/README.md | 1 + 8 files changed, 863 insertions(+), 4 deletions(-) create mode 100644 crates/bevy_core_pipeline/src/fxaa/fxaa.wgsl create mode 100644 crates/bevy_core_pipeline/src/fxaa/mod.rs create mode 100644 crates/bevy_core_pipeline/src/fxaa/node.rs create mode 100644 examples/3d/fxaa.rs diff --git a/Cargo.toml b/Cargo.toml index f4c6f3f9f9037..7f2d1fd25317b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -323,6 +323,16 @@ description = "Loads and renders a glTF file as a scene" category = "3D Rendering" wasm = true +[[example]] +name = "fxaa" +path = "examples/3d/fxaa.rs" + +[package.metadata.example.fxaa] +name = "FXAA" +description = "Compares MSAA (Multi-Sample Anti-Aliasing) and FXAA (Fast Approximate Anti-Aliasing)" +category = "3D Rendering" +wasm = true + [[example]] name = "msaa" path = "examples/3d/msaa.rs" diff --git a/crates/bevy_core_pipeline/src/fxaa/fxaa.wgsl b/crates/bevy_core_pipeline/src/fxaa/fxaa.wgsl new file mode 100644 index 0000000000000..1c24af6d80c32 --- /dev/null +++ b/crates/bevy_core_pipeline/src/fxaa/fxaa.wgsl @@ -0,0 +1,277 @@ +// NVIDIA FXAA 3.11 +// Original source code by TIMOTHY LOTTES +// https://gist.github.com/kosua20/0c506b81b3812ac900048059d2383126 +// +// Cleaned version - https://github.com/kosua20/Rendu/blob/master/resources/common/shaders/screens/fxaa.frag +// +// Tweaks by mrDIMAS - https://github.com/FyroxEngine/Fyrox/blob/master/src/renderer/shaders/fxaa_fs.glsl + +#import bevy_core_pipeline::fullscreen_vertex_shader + +@group(0) @binding(0) +var screenTexture: texture_2d; +@group(0) @binding(1) +var samp: sampler; + +// Trims the algorithm from processing darks. +#ifdef EDGE_THRESH_MIN_LOW + let EDGE_THRESHOLD_MIN: f32 = 0.0833; +#endif + +#ifdef EDGE_THRESH_MIN_MEDIUM + let EDGE_THRESHOLD_MIN: f32 = 0.0625; +#endif + +#ifdef EDGE_THRESH_MIN_HIGH + let EDGE_THRESHOLD_MIN: f32 = 0.0312; +#endif + +#ifdef EDGE_THRESH_MIN_ULTRA + let EDGE_THRESHOLD_MIN: f32 = 0.0156; +#endif + +#ifdef EDGE_THRESH_MIN_EXTREME + let EDGE_THRESHOLD_MIN: f32 = 0.0078; +#endif + +// The minimum amount of local contrast required to apply algorithm. +#ifdef EDGE_THRESH_LOW + let EDGE_THRESHOLD_MAX: f32 = 0.250; +#endif + +#ifdef EDGE_THRESH_MEDIUM + let EDGE_THRESHOLD_MAX: f32 = 0.166; +#endif + +#ifdef EDGE_THRESH_HIGH + let EDGE_THRESHOLD_MAX: f32 = 0.125; +#endif + +#ifdef EDGE_THRESH_ULTRA + let EDGE_THRESHOLD_MAX: f32 = 0.063; +#endif + +#ifdef EDGE_THRESH_EXTREME + let EDGE_THRESHOLD_MAX: f32 = 0.031; +#endif + +let ITERATIONS: i32 = 12; //default is 12 +let SUBPIXEL_QUALITY: f32 = 0.75; +// #define QUALITY(q) ((q) < 5 ? 1.0 : ((q) > 5 ? ((q) < 10 ? 2.0 : ((q) < 11 ? 4.0 : 8.0)) : 1.5)) +fn QUALITY(q: i32) -> f32 { + switch (q) { + //case 0, 1, 2, 3, 4: { return 1.0; } + default: { return 1.0; } + case 5: { return 1.5; } + case 6, 7, 8, 9: { return 2.0; } + case 10: { return 4.0; } + case 11: { return 8.0; } + } +} + +fn rgb2luma(rgb: vec3) -> f32 { + return sqrt(dot(rgb, vec3(0.299, 0.587, 0.114))); +} + +// Performs FXAA post-process anti-aliasing as described in the Nvidia FXAA white paper and the associated shader code. +@fragment +fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4 { + let resolution = vec2(textureDimensions(screenTexture)); + let fragCoord = in.position.xy; + let inverseScreenSize = 1.0 / resolution.xy; + let texCoord = in.position.xy * inverseScreenSize; + + let centerSample = textureSampleLevel(screenTexture, samp, texCoord, 0.0); + let colorCenter = centerSample.rgb; + + // Luma at the current fragment + let lumaCenter = rgb2luma(colorCenter); + + // Luma at the four direct neighbours of the current fragment. + let lumaDown = rgb2luma(textureSampleLevel(screenTexture, samp, texCoord, 0.0, vec2(0, -1)).rgb); + let lumaUp = rgb2luma(textureSampleLevel(screenTexture, samp, texCoord, 0.0, vec2(0, 1)).rgb); + let lumaLeft = rgb2luma(textureSampleLevel(screenTexture, samp, texCoord, 0.0, vec2(-1, 0)).rgb); + let lumaRight = rgb2luma(textureSampleLevel(screenTexture, samp, texCoord, 0.0, vec2(1, 0)).rgb); + + // Find the maximum and minimum luma around the current fragment. + let lumaMin = min(lumaCenter, min(min(lumaDown, lumaUp), min(lumaLeft, lumaRight))); + let lumaMax = max(lumaCenter, max(max(lumaDown, lumaUp), max(lumaLeft, lumaRight))); + + // Compute the delta. + let lumaRange = lumaMax - lumaMin; + + // If the luma variation is lower that a threshold (or if we are in a really dark area), we are not on an edge, don't perform any AA. + if (lumaRange < max(EDGE_THRESHOLD_MIN, lumaMax * EDGE_THRESHOLD_MAX)) { + return centerSample; + } + + // Query the 4 remaining corners lumas. + let lumaDownLeft = rgb2luma(textureSampleLevel(screenTexture, samp, texCoord, 0.0, vec2(-1, -1)).rgb); + let lumaUpRight = rgb2luma(textureSampleLevel(screenTexture, samp, texCoord, 0.0, vec2(1, 1)).rgb); + let lumaUpLeft = rgb2luma(textureSampleLevel(screenTexture, samp, texCoord, 0.0, vec2(-1, 1)).rgb); + let lumaDownRight = rgb2luma(textureSampleLevel(screenTexture, samp, texCoord, 0.0, vec2(1, -1)).rgb); + + // Combine the four edges lumas (using intermediary variables for future computations with the same values). + let lumaDownUp = lumaDown + lumaUp; + let lumaLeftRight = lumaLeft + lumaRight; + + // Same for corners + let lumaLeftCorners = lumaDownLeft + lumaUpLeft; + let lumaDownCorners = lumaDownLeft + lumaDownRight; + let lumaRightCorners = lumaDownRight + lumaUpRight; + let lumaUpCorners = lumaUpRight + lumaUpLeft; + + // Compute an estimation of the gradient along the horizontal and vertical axis. + let edgeHorizontal = abs(-2.0 * lumaLeft + lumaLeftCorners) + + abs(-2.0 * lumaCenter + lumaDownUp) * 2.0 + + abs(-2.0 * lumaRight + lumaRightCorners); + + let edgeVertical = abs(-2.0 * lumaUp + lumaUpCorners) + + abs(-2.0 * lumaCenter + lumaLeftRight) * 2.0 + + abs(-2.0 * lumaDown + lumaDownCorners); + + // Is the local edge horizontal or vertical ? + let isHorizontal = (edgeHorizontal >= edgeVertical); + + // Choose the step size (one pixel) accordingly. + var stepLength = select(inverseScreenSize.x, inverseScreenSize.y, isHorizontal); + + // Select the two neighboring texels lumas in the opposite direction to the local edge. + var luma1 = select(lumaLeft, lumaDown, isHorizontal); + var luma2 = select(lumaRight, lumaUp, isHorizontal); + + // Compute gradients in this direction. + let gradient1 = luma1 - lumaCenter; + let gradient2 = luma2 - lumaCenter; + + // Which direction is the steepest ? + let is1Steepest = abs(gradient1) >= abs(gradient2); + + // Gradient in the corresponding direction, normalized. + let gradientScaled = 0.25 * max(abs(gradient1), abs(gradient2)); + + // Average luma in the correct direction. + var lumaLocalAverage = 0.0; + if (is1Steepest) { + // Switch the direction + stepLength = -stepLength; + lumaLocalAverage = 0.5 * (luma1 + lumaCenter); + } else { + lumaLocalAverage = 0.5 * (luma2 + lumaCenter); + } + + // Shift UV in the correct direction by half a pixel. + // Compute offset (for each iteration step) in the right direction. + var currentUv = texCoord; + var offset = vec2(0.0, 0.0); + if (isHorizontal) { + currentUv.y = currentUv.y + stepLength * 0.5; + offset.x = inverseScreenSize.x; + } else { + currentUv.x = currentUv.x + stepLength * 0.5; + offset.y = inverseScreenSize.y; + } + + // Compute UVs to explore on each side of the edge, orthogonally. The QUALITY allows us to step faster. + var uv1 = currentUv - offset; // * QUALITY(0); // (quality 0 is 1.0) + var uv2 = currentUv + offset; // * QUALITY(0); // (quality 0 is 1.0) + + // Read the lumas at both current extremities of the exploration segment, and compute the delta wrt to the local average luma. + var lumaEnd1 = rgb2luma(textureSampleLevel(screenTexture, samp, uv1, 0.0).rgb); + var lumaEnd2 = rgb2luma(textureSampleLevel(screenTexture, samp, uv2, 0.0).rgb); + lumaEnd1 = lumaEnd1 - lumaLocalAverage; + lumaEnd2 = lumaEnd2 - lumaLocalAverage; + + // If the luma deltas at the current extremities is larger than the local gradient, we have reached the side of the edge. + var reached1 = abs(lumaEnd1) >= gradientScaled; + var reached2 = abs(lumaEnd2) >= gradientScaled; + var reachedBoth = reached1 && reached2; + + // If the side is not reached, we continue to explore in this direction. + uv1 = select(uv1 - offset, uv1, reached1); // * QUALITY(1); // (quality 1 is 1.0) + uv2 = select(uv2 - offset, uv2, reached2); // * QUALITY(1); // (quality 1 is 1.0) + + // If both sides have not been reached, continue to explore. + if (!reachedBoth) { + for (var i: i32 = 2; i < ITERATIONS; i = i + 1) { + // If needed, read luma in 1st direction, compute delta. + if (!reached1) { + lumaEnd1 = rgb2luma(textureSampleLevel(screenTexture, samp, uv1, 0.0).rgb); + lumaEnd1 = lumaEnd1 - lumaLocalAverage; + } + // If needed, read luma in opposite direction, compute delta. + if (!reached2) { + lumaEnd2 = rgb2luma(textureSampleLevel(screenTexture, samp, uv2, 0.0).rgb); + lumaEnd2 = lumaEnd2 - lumaLocalAverage; + } + // If the luma deltas at the current extremities is larger than the local gradient, we have reached the side of the edge. + reached1 = abs(lumaEnd1) >= gradientScaled; + reached2 = abs(lumaEnd2) >= gradientScaled; + reachedBoth = reached1 && reached2; + + // If the side is not reached, we continue to explore in this direction, with a variable quality. + if (!reached1) { + uv1 = uv1 - offset * QUALITY(i); + } + if (!reached2) { + uv2 = uv2 + offset * QUALITY(i); + } + + // If both sides have been reached, stop the exploration. + if (reachedBoth) { + break; + } + } + } + + // Compute the distances to each side edge of the edge (!). + var distance1 = select(texCoord.y - uv1.y, texCoord.x - uv1.x, isHorizontal); + var distance2 = select(uv2.y - texCoord.y, uv2.x - texCoord.x, isHorizontal); + + // In which direction is the side of the edge closer ? + let isDirection1 = distance1 < distance2; + let distanceFinal = min(distance1, distance2); + + // Thickness of the edge. + let edgeThickness = (distance1 + distance2); + + // Is the luma at center smaller than the local average ? + let isLumaCenterSmaller = lumaCenter < lumaLocalAverage; + + // If the luma at center is smaller than at its neighbour, the delta luma at each end should be positive (same variation). + let correctVariation1 = (lumaEnd1 < 0.0) != isLumaCenterSmaller; + let correctVariation2 = (lumaEnd2 < 0.0) != isLumaCenterSmaller; + + // Only keep the result in the direction of the closer side of the edge. + var correctVariation = select(correctVariation2, correctVariation1, isDirection1); + + // UV offset: read in the direction of the closest side of the edge. + let pixelOffset = - distanceFinal / edgeThickness + 0.5; + + // If the luma variation is incorrect, do not offset. + var finalOffset = select(0.0, pixelOffset, correctVariation); + + // Sub-pixel shifting + // Full weighted average of the luma over the 3x3 neighborhood. + let lumaAverage = (1.0 / 12.0) * (2.0 * (lumaDownUp + lumaLeftRight) + lumaLeftCorners + lumaRightCorners); + // Ratio of the delta between the global average and the center luma, over the luma range in the 3x3 neighborhood. + let subPixelOffset1 = clamp(abs(lumaAverage - lumaCenter) / lumaRange, 0.0, 1.0); + let subPixelOffset2 = (-2.0 * subPixelOffset1 + 3.0) * subPixelOffset1 * subPixelOffset1; + // Compute a sub-pixel offset based on this delta. + let subPixelOffsetFinal = subPixelOffset2 * subPixelOffset2 * SUBPIXEL_QUALITY; + + // Pick the biggest of the two offsets. + finalOffset = max(finalOffset, subPixelOffsetFinal); + + // Compute the final UV coordinates. + var finalUv = texCoord; + if (isHorizontal) { + finalUv.y = finalUv.y + finalOffset * stepLength; + } else { + finalUv.x = finalUv.x + finalOffset * stepLength; + } + + // Read the color at the new UV coordinates, and use it. + var finalColor = textureSampleLevel(screenTexture, samp, finalUv, 0.0).rgb; + return vec4(finalColor, centerSample.a); +} diff --git a/crates/bevy_core_pipeline/src/fxaa/mod.rs b/crates/bevy_core_pipeline/src/fxaa/mod.rs new file mode 100644 index 0000000000000..61f9aa17c770d --- /dev/null +++ b/crates/bevy_core_pipeline/src/fxaa/mod.rs @@ -0,0 +1,249 @@ +mod node; + +use crate::{ + core_2d, core_3d, fullscreen_vertex_shader::fullscreen_shader_vertex_state, + fxaa::node::FxaaNode, +}; +use bevy_app::prelude::*; +use bevy_asset::{load_internal_asset, HandleUntyped}; +use bevy_derive::Deref; +use bevy_ecs::{prelude::*, query::QueryItem}; +use bevy_reflect::TypeUuid; +use bevy_render::{ + extract_component::{ExtractComponent, ExtractComponentPlugin}, + prelude::Camera, + render_graph::RenderGraph, + render_resource::*, + renderer::RenderDevice, + texture::BevyDefault, + view::{ExtractedView, ViewTarget}, + RenderApp, RenderStage, +}; + +#[derive(Eq, PartialEq, Hash, Clone, Copy)] +pub enum Sensitivity { + Low, + Medium, + High, + Ultra, + Extreme, +} + +impl Sensitivity { + pub fn get_str(&self) -> &str { + match self { + Sensitivity::Low => "LOW", + Sensitivity::Medium => "MEDIUM", + Sensitivity::High => "HIGH", + Sensitivity::Ultra => "ULTRA", + Sensitivity::Extreme => "EXTREME", + } + } +} + +#[derive(Component, Clone)] +pub struct Fxaa { + /// Enable render passes for FXAA. + pub enabled: bool, + + /// Use lower sensitivity for a sharper, faster, result. + /// Use higher sensitivity for a slower, smoother, result. + /// Ultra and Turbo settings can result in significant smearing and loss of detail. + + /// The minimum amount of local contrast required to apply algorithm. + pub edge_threshold: Sensitivity, + + /// Trims the algorithm from processing darks. + pub edge_threshold_min: Sensitivity, +} + +impl Default for Fxaa { + fn default() -> Self { + Fxaa { + enabled: true, + edge_threshold: Sensitivity::High, + edge_threshold_min: Sensitivity::High, + } + } +} + +impl ExtractComponent for Fxaa { + type Query = &'static Self; + type Filter = With; + + fn extract_component(item: QueryItem) -> Self { + item.clone() + } +} + +const FXAA_SHADER_HANDLE: HandleUntyped = + HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 4182761465141723543); + +pub const FXAA_NODE_3D: &str = "fxaa_node_3d"; +pub const FXAA_NODE_2D: &str = "fxaa_node_2d"; + +/// Adds support for Fast Approximate Anti-Aliasing (FXAA) +pub struct FxaaPlugin; +impl Plugin for FxaaPlugin { + fn build(&self, app: &mut App) { + load_internal_asset!(app, FXAA_SHADER_HANDLE, "fxaa.wgsl", Shader::from_wgsl); + + app.add_plugin(ExtractComponentPlugin::::default()); + + let render_app = match app.get_sub_app_mut(RenderApp) { + Ok(render_app) => render_app, + Err(_) => return, + }; + render_app + .init_resource::() + .init_resource::>() + .add_system_to_stage(RenderStage::Prepare, prepare_fxaa_pipelines); + + { + let fxaa_node = FxaaNode::new(&mut render_app.world); + let mut binding = render_app.world.resource_mut::(); + let graph = binding.get_sub_graph_mut(core_3d::graph::NAME).unwrap(); + + graph.add_node(FXAA_NODE_3D, fxaa_node); + + graph + .add_slot_edge( + graph.input_node().unwrap().id, + core_3d::graph::input::VIEW_ENTITY, + FXAA_NODE_3D, + FxaaNode::IN_VIEW, + ) + .unwrap(); + + graph + .add_node_edge(core_3d::graph::node::TONEMAPPING, FXAA_NODE_3D) + .unwrap(); + } + { + let fxaa_node = FxaaNode::new(&mut render_app.world); + let mut binding = render_app.world.resource_mut::(); + let graph = binding.get_sub_graph_mut(core_2d::graph::NAME).unwrap(); + + graph.add_node(FXAA_NODE_2D, fxaa_node); + + graph + .add_slot_edge( + graph.input_node().unwrap().id, + core_2d::graph::input::VIEW_ENTITY, + FXAA_NODE_2D, + FxaaNode::IN_VIEW, + ) + .unwrap(); + + graph + .add_node_edge(core_2d::graph::node::TONEMAPPING, FXAA_NODE_2D) + .unwrap(); + } + } +} + +#[derive(Resource, Deref)] +pub struct FxaaPipeline { + texture_bind_group: BindGroupLayout, +} + +impl FromWorld for FxaaPipeline { + fn from_world(render_world: &mut World) -> Self { + let texture_bind_group = render_world + .resource::() + .create_bind_group_layout(&BindGroupLayoutDescriptor { + label: Some("fxaa_texture_bind_group_layout"), + entries: &[ + BindGroupLayoutEntry { + binding: 0, + visibility: ShaderStages::FRAGMENT, + ty: BindingType::Texture { + sample_type: TextureSampleType::Float { filterable: true }, + view_dimension: TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }, + BindGroupLayoutEntry { + binding: 1, + visibility: ShaderStages::FRAGMENT, + ty: BindingType::Sampler(SamplerBindingType::Filtering), + count: None, + }, + ], + }); + + FxaaPipeline { texture_bind_group } + } +} + +#[derive(Component)] +pub struct CameraFxaaPipeline { + pub pipeline_id: CachedRenderPipelineId, +} + +#[derive(PartialEq, Eq, Hash, Clone, Copy)] +pub struct FxaaPipelineKey { + edge_threshold: Sensitivity, + edge_threshold_min: Sensitivity, + texture_format: TextureFormat, +} + +impl SpecializedRenderPipeline for FxaaPipeline { + type Key = FxaaPipelineKey; + + fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor { + RenderPipelineDescriptor { + label: Some("fxaa".into()), + layout: Some(vec![self.texture_bind_group.clone()]), + vertex: fullscreen_shader_vertex_state(), + fragment: Some(FragmentState { + shader: FXAA_SHADER_HANDLE.typed(), + shader_defs: vec![ + format!("EDGE_THRESH_{}", key.edge_threshold.get_str()), + format!("EDGE_THRESH_MIN_{}", key.edge_threshold_min.get_str()), + ], + entry_point: "fragment".into(), + targets: vec![Some(ColorTargetState { + format: key.texture_format, + blend: None, + write_mask: ColorWrites::ALL, + })], + }), + primitive: PrimitiveState::default(), + depth_stencil: None, + multisample: MultisampleState::default(), + } + } +} + +pub fn prepare_fxaa_pipelines( + mut commands: Commands, + mut pipeline_cache: ResMut, + mut pipelines: ResMut>, + fxaa_pipeline: Res, + views: Query<(Entity, &ExtractedView, &Fxaa)>, +) { + for (entity, view, fxaa) in &views { + if !fxaa.enabled { + continue; + } + let pipeline_id = pipelines.specialize( + &mut pipeline_cache, + &fxaa_pipeline, + FxaaPipelineKey { + edge_threshold: fxaa.edge_threshold, + edge_threshold_min: fxaa.edge_threshold_min, + texture_format: if view.hdr { + ViewTarget::TEXTURE_FORMAT_HDR + } else { + TextureFormat::bevy_default() + }, + }, + ); + + commands + .entity(entity) + .insert(CameraFxaaPipeline { pipeline_id }); + } +} diff --git a/crates/bevy_core_pipeline/src/fxaa/node.rs b/crates/bevy_core_pipeline/src/fxaa/node.rs new file mode 100644 index 0000000000000..6e12151c2fe63 --- /dev/null +++ b/crates/bevy_core_pipeline/src/fxaa/node.rs @@ -0,0 +1,132 @@ +use std::sync::Mutex; + +use crate::fxaa::{CameraFxaaPipeline, Fxaa, FxaaPipeline}; +use bevy_ecs::prelude::*; +use bevy_ecs::query::QueryState; +use bevy_render::{ + render_graph::{Node, NodeRunError, RenderGraphContext, SlotInfo, SlotType}, + render_resource::{ + BindGroup, BindGroupDescriptor, BindGroupEntry, BindingResource, FilterMode, Operations, + PipelineCache, RenderPassColorAttachment, RenderPassDescriptor, SamplerDescriptor, + TextureViewId, + }, + renderer::RenderContext, + view::{ExtractedView, ViewTarget}, +}; +use bevy_utils::default; + +pub struct FxaaNode { + query: QueryState< + ( + &'static ViewTarget, + &'static CameraFxaaPipeline, + &'static Fxaa, + ), + With, + >, + cached_texture_bind_group: Mutex>, +} + +impl FxaaNode { + pub const IN_VIEW: &'static str = "view"; + + pub fn new(world: &mut World) -> Self { + Self { + query: QueryState::new(world), + cached_texture_bind_group: Mutex::new(None), + } + } +} + +impl Node for FxaaNode { + fn input(&self) -> Vec { + vec![SlotInfo::new(FxaaNode::IN_VIEW, SlotType::Entity)] + } + + fn update(&mut self, world: &mut World) { + self.query.update_archetypes(world); + } + + fn run( + &self, + graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + world: &World, + ) -> Result<(), NodeRunError> { + let view_entity = graph.get_input_entity(Self::IN_VIEW)?; + let pipeline_cache = world.resource::(); + let fxaa_pipeline = world.resource::(); + + let (target, pipeline, fxaa) = match self.query.get_manual(world, view_entity) { + Ok(result) => result, + Err(_) => return Ok(()), + }; + + if !fxaa.enabled { + return Ok(()); + }; + + let pipeline = pipeline_cache + .get_render_pipeline(pipeline.pipeline_id) + .unwrap(); + + let post_process = target.post_process_write(); + let source = post_process.source; + let destination = post_process.destination; + let mut cached_bind_group = self.cached_texture_bind_group.lock().unwrap(); + let bind_group = match &mut *cached_bind_group { + Some((id, bind_group)) if source.id() == *id => bind_group, + cached_bind_group => { + let sampler = render_context + .render_device + .create_sampler(&SamplerDescriptor { + mipmap_filter: FilterMode::Linear, + mag_filter: FilterMode::Linear, + min_filter: FilterMode::Linear, + ..default() + }); + + let bind_group = + render_context + .render_device + .create_bind_group(&BindGroupDescriptor { + label: None, + layout: &fxaa_pipeline.texture_bind_group, + entries: &[ + BindGroupEntry { + binding: 0, + resource: BindingResource::TextureView(source), + }, + BindGroupEntry { + binding: 1, + resource: BindingResource::Sampler(&sampler), + }, + ], + }); + + let (_, bind_group) = cached_bind_group.insert((source.id(), bind_group)); + bind_group + } + }; + + let pass_descriptor = RenderPassDescriptor { + label: Some("fxaa_pass"), + color_attachments: &[Some(RenderPassColorAttachment { + view: destination, + resolve_target: None, + ops: Operations::default(), + })], + depth_stencil_attachment: None, + }; + + let mut render_pass = render_context + .command_encoder + .begin_render_pass(&pass_descriptor); + + render_pass.set_pipeline(pipeline); + render_pass.set_bind_group(0, bind_group, &[]); + render_pass.draw(0..3, 0..1); + + Ok(()) + } +} diff --git a/crates/bevy_core_pipeline/src/lib.rs b/crates/bevy_core_pipeline/src/lib.rs index b46dba079b0cb..85f2c213ae344 100644 --- a/crates/bevy_core_pipeline/src/lib.rs +++ b/crates/bevy_core_pipeline/src/lib.rs @@ -2,6 +2,7 @@ pub mod clear_color; pub mod core_2d; pub mod core_3d; pub mod fullscreen_vertex_shader; +pub mod fxaa; pub mod tonemapping; pub mod upscaling; @@ -19,6 +20,7 @@ use crate::{ core_2d::Core2dPlugin, core_3d::Core3dPlugin, fullscreen_vertex_shader::FULLSCREEN_SHADER_HANDLE, + fxaa::FxaaPlugin, tonemapping::TonemappingPlugin, upscaling::UpscalingPlugin, }; @@ -45,6 +47,7 @@ impl Plugin for CorePipelinePlugin { .add_plugin(TonemappingPlugin) .add_plugin(UpscalingPlugin) .add_plugin(Core2dPlugin) - .add_plugin(Core3dPlugin); + .add_plugin(Core3dPlugin) + .add_plugin(FxaaPlugin); } } diff --git a/crates/bevy_pbr/src/render/pbr.wgsl b/crates/bevy_pbr/src/render/pbr.wgsl index b11700342f79f..1b943c666f330 100644 --- a/crates/bevy_pbr/src/render/pbr.wgsl +++ b/crates/bevy_pbr/src/render/pbr.wgsl @@ -88,12 +88,12 @@ fn fragment(in: FragmentInput) -> @location(0) vec4 { ); pbr_input.V = calculate_view(in.world_position, pbr_input.is_orthographic); output_color = pbr(pbr_input); -#ifdef TONEMAP_IN_SHADER - output_color = tone_mapping(output_color); -#endif } else { output_color = alpha_discard(material, output_color); } +#ifdef TONEMAP_IN_SHADER + output_color = tone_mapping(output_color); +#endif return output_color; } diff --git a/examples/3d/fxaa.rs b/examples/3d/fxaa.rs new file mode 100644 index 0000000000000..9a51145badb35 --- /dev/null +++ b/examples/3d/fxaa.rs @@ -0,0 +1,187 @@ +//! This examples compares MSAA (Multi-Sample Anti-Aliasing) and FXAA (Fast Approximate Anti-Aliasing). + +use std::f32::consts::PI; + +use bevy::{ + core_pipeline::fxaa::{Fxaa, Sensitivity}, + prelude::*, + render::{ + render_resource::{Extent3d, SamplerDescriptor, TextureDimension, TextureFormat}, + texture::ImageSampler, + }, +}; + +fn main() { + App::new() + // Disable MSAA be default + .insert_resource(Msaa { samples: 1 }) + .add_plugins(DefaultPlugins) + .add_startup_system(setup) + .add_system(toggle_fxaa) + .run(); +} + +/// set up a simple 3D scene +fn setup( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, + mut images: ResMut>, + asset_server: Res, +) { + println!("Toggle with:"); + println!("1 - NO AA"); + println!("2 - MSAA 4"); + println!("3 - FXAA (default)"); + + println!("Threshold:"); + println!("6 - LOW"); + println!("7 - MEDIUM"); + println!("8 - HIGH (default)"); + println!("9 - ULTRA"); + println!("0 - EXTREME"); + + // plane + commands.spawn(PbrBundle { + mesh: meshes.add(Mesh::from(shape::Plane { size: 5.0 })), + material: materials.add(Color::rgb(0.3, 0.5, 0.3).into()), + ..default() + }); + + let cube_material = materials.add(StandardMaterial { + base_color_texture: Some(images.add(uv_debug_texture())), + ..default() + }); + + // cubes + for i in 0..5 { + commands.spawn(PbrBundle { + mesh: meshes.add(Mesh::from(shape::Cube { size: 0.25 })), + material: cube_material.clone(), + transform: Transform::from_xyz(i as f32 * 0.25 - 1.0, 0.125, -i as f32 * 0.5), + ..default() + }); + } + + // Flight Helmet + commands.spawn(SceneBundle { + scene: asset_server.load("models/FlightHelmet/FlightHelmet.gltf#Scene0"), + ..default() + }); + + // light + const HALF_SIZE: f32 = 2.0; + commands.spawn(DirectionalLightBundle { + directional_light: DirectionalLight { + shadow_projection: OrthographicProjection { + left: -HALF_SIZE, + right: HALF_SIZE, + bottom: -HALF_SIZE, + top: HALF_SIZE, + near: -10.0 * HALF_SIZE, + far: 10.0 * HALF_SIZE, + ..default() + }, + shadows_enabled: true, + ..default() + }, + transform: Transform::from_rotation(Quat::from_euler( + EulerRot::ZYX, + 0.0, + PI * -0.15, + PI * -0.15, + )), + ..default() + }); + + // camera + commands + .spawn(Camera3dBundle { + camera: Camera { + hdr: false, // Works with and without hdr + ..default() + }, + transform: Transform::from_xyz(0.7, 0.7, 1.0) + .looking_at(Vec3::new(0.0, 0.3, 0.0), Vec3::Y), + ..default() + }) + .insert(Fxaa::default()); +} + +fn toggle_fxaa(keys: Res>, mut query: Query<&mut Fxaa>, mut msaa: ResMut) { + let set_no_aa = keys.just_pressed(KeyCode::Key1); + let set_msaa = keys.just_pressed(KeyCode::Key2); + let set_fxaa = keys.just_pressed(KeyCode::Key3); + let fxaa_low = keys.just_pressed(KeyCode::Key6); + let fxaa_med = keys.just_pressed(KeyCode::Key7); + let fxaa_high = keys.just_pressed(KeyCode::Key8); + let fxaa_ultra = keys.just_pressed(KeyCode::Key9); + let fxaa_extreme = keys.just_pressed(KeyCode::Key0); + let set_fxaa = set_fxaa | fxaa_low | fxaa_med | fxaa_high | fxaa_ultra | fxaa_extreme; + for mut fxaa in &mut query { + if set_msaa { + fxaa.enabled = false; + msaa.samples = 4; + info!("MSAA 4x"); + } + if set_no_aa { + fxaa.enabled = false; + msaa.samples = 1; + info!("NO AA"); + } + if set_no_aa | set_fxaa { + msaa.samples = 1; + } + if fxaa_low { + fxaa.edge_threshold = Sensitivity::Low; + fxaa.edge_threshold_min = Sensitivity::Low; + } else if fxaa_med { + fxaa.edge_threshold = Sensitivity::Medium; + fxaa.edge_threshold_min = Sensitivity::Medium; + } else if fxaa_high { + fxaa.edge_threshold = Sensitivity::High; + fxaa.edge_threshold_min = Sensitivity::High; + } else if fxaa_ultra { + fxaa.edge_threshold = Sensitivity::Ultra; + fxaa.edge_threshold_min = Sensitivity::Ultra; + } else if fxaa_extreme { + fxaa.edge_threshold = Sensitivity::Extreme; + fxaa.edge_threshold_min = Sensitivity::Extreme; + } + if set_fxaa { + fxaa.enabled = true; + msaa.samples = 1; + info!("FXAA {}", fxaa.edge_threshold.get_str()); + } + } +} + +/// Creates a colorful test pattern +fn uv_debug_texture() -> Image { + const TEXTURE_SIZE: usize = 8; + + let mut palette: [u8; 32] = [ + 255, 102, 159, 255, 255, 159, 102, 255, 236, 255, 102, 255, 121, 255, 102, 255, 102, 255, + 198, 255, 102, 198, 255, 255, 121, 102, 255, 255, 236, 102, 255, 255, + ]; + + let mut texture_data = [0; TEXTURE_SIZE * TEXTURE_SIZE * 4]; + for y in 0..TEXTURE_SIZE { + let offset = TEXTURE_SIZE * y * 4; + texture_data[offset..(offset + TEXTURE_SIZE * 4)].copy_from_slice(&palette); + palette.rotate_right(4); + } + + let mut img = Image::new_fill( + Extent3d { + width: TEXTURE_SIZE as u32, + height: TEXTURE_SIZE as u32, + depth_or_array_layers: 1, + }, + TextureDimension::D2, + &texture_data, + TextureFormat::Rgba8UnormSrgb, + ); + img.sampler_descriptor = ImageSampler::Descriptor(SamplerDescriptor::default()); + img +} diff --git a/examples/README.md b/examples/README.md index e5e1139785669..c2d00514e0c40 100644 --- a/examples/README.md +++ b/examples/README.md @@ -106,6 +106,7 @@ Example | Description --- | --- [3D Scene](../examples/3d/3d_scene.rs) | Simple 3D scene with basic shapes and lighting [3D Shapes](../examples/3d/3d_shapes.rs) | A scene showcasing the built-in 3D shapes +[FXAA](../examples/3d/fxaa.rs) | Compares MSAA (Multi-Sample Anti-Aliasing) and FXAA (Fast Approximate Anti-Aliasing) [Lighting](../examples/3d/lighting.rs) | Illustrates various lighting options in a simple scene [Lines](../examples/3d/lines.rs) | Create a custom material to draw 3d lines [Load glTF](../examples/3d/load_gltf.rs) | Loads and renders a glTF file as a scene