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