bevy_core_pipeline/smaa/
mod.rs

1//! Subpixel morphological antialiasing (SMAA).
2//!
3//! [SMAA] is a 2011 antialiasing technique that takes an aliased image and
4//! smooths out the *jaggies*, making edges smoother. It's been used in numerous
5//! games and has become a staple postprocessing technique. Compared to MSAA,
6//! SMAA has the advantage of compatibility with deferred rendering and
7//! reduction of GPU memory bandwidth.  Compared to FXAA, SMAA has the advantage
8//! of improved quality, but the disadvantage of reduced performance. Compared
9//! to TAA, SMAA has the advantage of stability and lack of *ghosting*
10//! artifacts, but has the disadvantage of not supporting temporal accumulation,
11//! which have made SMAA less popular when advanced photorealistic rendering
12//! features are used in recent years.
13//!
14//! To use SMAA, add [`Smaa`] to a [`bevy_render::camera::Camera`]. In a
15//! pinch, you can simply use the default settings (via the [`Default`] trait)
16//! for a high-quality, high-performance appearance. When using SMAA, you will
17//! likely want set [`bevy_render::view::Msaa`] to [`bevy_render::view::Msaa::Off`]
18//! for every camera using SMAA.
19//!
20//! Those who have used SMAA in other engines should be aware that Bevy doesn't
21//! yet support the following more advanced features of SMAA:
22//!
23//! * The temporal variant.
24//!
25//! * Depth- and chroma-based edge detection.
26//!
27//! * Predicated thresholding.
28//!
29//! * Compatibility with SSAA and MSAA.
30//!
31//! [SMAA]: https://www.iryoku.com/smaa/
32#[cfg(not(feature = "smaa_luts"))]
33use crate::tonemapping::lut_placeholder;
34use crate::{
35    core_2d::graph::{Core2d, Node2d},
36    core_3d::graph::{Core3d, Node3d},
37};
38use bevy_app::{App, Plugin};
39#[cfg(feature = "smaa_luts")]
40use bevy_asset::load_internal_binary_asset;
41use bevy_asset::{load_internal_asset, Handle};
42use bevy_derive::{Deref, DerefMut};
43use bevy_ecs::{
44    component::Component,
45    entity::Entity,
46    query::{QueryItem, With},
47    reflect::ReflectComponent,
48    schedule::IntoSystemConfigs as _,
49    system::{lifetimeless::Read, Commands, Query, Res, ResMut, Resource},
50    world::{FromWorld, World},
51};
52use bevy_image::{BevyDefault, Image};
53use bevy_math::{vec4, Vec4};
54use bevy_reflect::{std_traits::ReflectDefault, Reflect};
55use bevy_render::{
56    camera::ExtractedCamera,
57    extract_component::{ExtractComponent, ExtractComponentPlugin},
58    render_asset::RenderAssets,
59    render_graph::{
60        NodeRunError, RenderGraphApp as _, RenderGraphContext, ViewNode, ViewNodeRunner,
61    },
62    render_resource::{
63        binding_types::{sampler, texture_2d, uniform_buffer},
64        AddressMode, BindGroup, BindGroupEntries, BindGroupLayout, BindGroupLayoutEntries,
65        CachedRenderPipelineId, ColorTargetState, ColorWrites, CompareFunction, DepthStencilState,
66        DynamicUniformBuffer, Extent3d, FilterMode, FragmentState, LoadOp, MultisampleState,
67        Operations, PipelineCache, PrimitiveState, RenderPassColorAttachment,
68        RenderPassDepthStencilAttachment, RenderPassDescriptor, RenderPipeline,
69        RenderPipelineDescriptor, SamplerBindingType, SamplerDescriptor, Shader, ShaderDefVal,
70        ShaderStages, ShaderType, SpecializedRenderPipeline, SpecializedRenderPipelines,
71        StencilFaceState, StencilOperation, StencilState, StoreOp, TextureDescriptor,
72        TextureDimension, TextureFormat, TextureSampleType, TextureUsages, TextureView,
73        VertexState,
74    },
75    renderer::{RenderContext, RenderDevice, RenderQueue},
76    texture::{CachedTexture, GpuImage, TextureCache},
77    view::{ExtractedView, ViewTarget},
78    Render, RenderApp, RenderSet,
79};
80use bevy_utils::prelude::default;
81
82/// The handle of the `smaa.wgsl` shader.
83const SMAA_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(12247928498010601081);
84/// The handle of the area LUT, a KTX2 format texture that SMAA uses internally.
85const SMAA_AREA_LUT_TEXTURE_HANDLE: Handle<Image> = Handle::weak_from_u128(15283551734567401670);
86/// The handle of the search LUT, a KTX2 format texture that SMAA uses internally.
87const SMAA_SEARCH_LUT_TEXTURE_HANDLE: Handle<Image> = Handle::weak_from_u128(3187314362190283210);
88
89/// Adds support for subpixel morphological antialiasing, or SMAA.
90pub struct SmaaPlugin;
91
92/// A component for enabling Subpixel Morphological Anti-Aliasing (SMAA)
93/// for a [`bevy_render::camera::Camera`].
94#[derive(Clone, Copy, Default, Component, Reflect, ExtractComponent)]
95#[reflect(Component, Default)]
96#[doc(alias = "SubpixelMorphologicalAntiAliasing")]
97pub struct Smaa {
98    /// A predefined set of SMAA parameters: i.e. a quality level.
99    ///
100    /// Generally, you can leave this at its default level.
101    pub preset: SmaaPreset,
102}
103
104#[deprecated(since = "0.15.0", note = "Renamed to `Smaa`")]
105pub type SmaaSettings = Smaa;
106
107/// A preset quality level for SMAA.
108///
109/// Higher values are slower but result in a higher-quality image.
110///
111/// The default value is *high*.
112#[derive(Clone, Copy, Reflect, Default, PartialEq, Eq, Hash)]
113#[reflect(Default)]
114pub enum SmaaPreset {
115    /// Four search steps; no diagonal or corner detection.
116    Low,
117
118    /// Eight search steps; no diagonal or corner detection.
119    Medium,
120
121    /// Sixteen search steps, 8 diagonal search steps, and corner detection.
122    ///
123    /// This is the default.
124    #[default]
125    High,
126
127    /// Thirty-two search steps, 8 diagonal search steps, and corner detection.
128    Ultra,
129}
130
131/// A render world resource that holds all render pipeline data needed for SMAA.
132///
133/// There are three separate passes, so we need three separate pipelines.
134#[derive(Resource)]
135pub struct SmaaPipelines {
136    /// Pass 1: Edge detection.
137    edge_detection: SmaaEdgeDetectionPipeline,
138    /// Pass 2: Blending weight calculation.
139    blending_weight_calculation: SmaaBlendingWeightCalculationPipeline,
140    /// Pass 3: Neighborhood blending.
141    neighborhood_blending: SmaaNeighborhoodBlendingPipeline,
142}
143
144/// The pipeline data for phase 1 of SMAA: edge detection.
145struct SmaaEdgeDetectionPipeline {
146    /// The bind group layout common to all passes.
147    postprocess_bind_group_layout: BindGroupLayout,
148    /// The bind group layout for data specific to this pass.
149    edge_detection_bind_group_layout: BindGroupLayout,
150}
151
152/// The pipeline data for phase 2 of SMAA: blending weight calculation.
153struct SmaaBlendingWeightCalculationPipeline {
154    /// The bind group layout common to all passes.
155    postprocess_bind_group_layout: BindGroupLayout,
156    /// The bind group layout for data specific to this pass.
157    blending_weight_calculation_bind_group_layout: BindGroupLayout,
158}
159
160/// The pipeline data for phase 3 of SMAA: neighborhood blending.
161struct SmaaNeighborhoodBlendingPipeline {
162    /// The bind group layout common to all passes.
163    postprocess_bind_group_layout: BindGroupLayout,
164    /// The bind group layout for data specific to this pass.
165    neighborhood_blending_bind_group_layout: BindGroupLayout,
166}
167
168/// A unique identifier for a set of SMAA pipelines.
169#[derive(Clone, PartialEq, Eq, Hash)]
170pub struct SmaaNeighborhoodBlendingPipelineKey {
171    /// The format of the framebuffer.
172    texture_format: TextureFormat,
173    /// The quality preset.
174    preset: SmaaPreset,
175}
176
177/// A render world component that holds the pipeline IDs for the SMAA passes.
178///
179/// There are three separate SMAA passes, each with a different shader and bind
180/// group layout, so we need three pipeline IDs.
181#[derive(Component)]
182pub struct ViewSmaaPipelines {
183    /// The pipeline ID for edge detection (phase 1).
184    edge_detection_pipeline_id: CachedRenderPipelineId,
185    /// The pipeline ID for blending weight calculation (phase 2).
186    blending_weight_calculation_pipeline_id: CachedRenderPipelineId,
187    /// The pipeline ID for neighborhood blending (phase 3).
188    neighborhood_blending_pipeline_id: CachedRenderPipelineId,
189}
190
191/// The render graph node that performs subpixel morphological antialiasing
192/// (SMAA).
193#[derive(Default)]
194pub struct SmaaNode;
195
196/// Values supplied to the GPU for SMAA.
197///
198/// Currently, this just contains the render target metrics and values derived
199/// from them. These could be computed by the shader itself, but the original
200/// SMAA HLSL code supplied them in a uniform, so we do the same for
201/// consistency.
202#[derive(Clone, Copy, ShaderType)]
203pub struct SmaaInfoUniform {
204    /// Information about the width and height of the framebuffer.
205    ///
206    /// * *x*: The reciprocal pixel width of the framebuffer.
207    ///
208    /// * *y*: The reciprocal pixel height of the framebuffer.
209    ///
210    /// * *z*: The pixel width of the framebuffer.
211    ///
212    /// * *w*: The pixel height of the framebuffer.
213    pub rt_metrics: Vec4,
214}
215
216/// A render world component that stores the offset of each [`SmaaInfoUniform`]
217/// within the [`SmaaInfoUniformBuffer`] for each view.
218#[derive(Clone, Copy, Deref, DerefMut, Component)]
219pub struct SmaaInfoUniformOffset(pub u32);
220
221/// The GPU buffer that holds all [`SmaaInfoUniform`]s for all views.
222///
223/// This is a resource stored in the render world.
224#[derive(Resource, Default, Deref, DerefMut)]
225pub struct SmaaInfoUniformBuffer(pub DynamicUniformBuffer<SmaaInfoUniform>);
226
227/// A render world component that holds the intermediate textures necessary to
228/// perform SMAA.
229///
230/// This is stored on each view that has enabled SMAA.
231#[derive(Component)]
232pub struct SmaaTextures {
233    /// The two-channel texture that stores the output from the first pass (edge
234    /// detection).
235    ///
236    /// The second pass (blending weight calculation) reads this texture to do
237    /// its work.
238    pub edge_detection_color_texture: CachedTexture,
239
240    /// The 8-bit stencil texture that records which pixels the first pass
241    /// touched, so that the second pass doesn't have to examine other pixels.
242    ///
243    /// Each texel will contain a 0 if the first pass didn't touch the
244    /// corresponding pixel or a 1 if the first pass did touch that pixel.
245    pub edge_detection_stencil_texture: CachedTexture,
246
247    /// A four-channel RGBA texture that stores the output from the second pass
248    /// (blending weight calculation).
249    ///
250    /// The final pass (neighborhood blending) reads this texture to do its
251    /// work.
252    pub blend_texture: CachedTexture,
253}
254
255/// A render world component that stores the bind groups necessary to perform
256/// SMAA.
257///
258/// This is stored on each view.
259#[derive(Component)]
260pub struct SmaaBindGroups {
261    /// The bind group for the first pass (edge detection).
262    pub edge_detection_bind_group: BindGroup,
263    /// The bind group for the second pass (blending weight calculation).
264    pub blending_weight_calculation_bind_group: BindGroup,
265    /// The bind group for the final pass (neighborhood blending).
266    pub neighborhood_blending_bind_group: BindGroup,
267}
268
269/// Stores the specialized render pipelines for SMAA.
270///
271/// Because SMAA uses three passes, we need three separate render pipeline
272/// stores.
273#[derive(Resource, Default)]
274pub struct SmaaSpecializedRenderPipelines {
275    /// Specialized render pipelines for the first phase (edge detection).
276    edge_detection: SpecializedRenderPipelines<SmaaEdgeDetectionPipeline>,
277
278    /// Specialized render pipelines for the second phase (blending weight
279    /// calculation).
280    blending_weight_calculation: SpecializedRenderPipelines<SmaaBlendingWeightCalculationPipeline>,
281
282    /// Specialized render pipelines for the third phase (neighborhood
283    /// blending).
284    neighborhood_blending: SpecializedRenderPipelines<SmaaNeighborhoodBlendingPipeline>,
285}
286
287impl Plugin for SmaaPlugin {
288    fn build(&self, app: &mut App) {
289        // Load the shader.
290        load_internal_asset!(app, SMAA_SHADER_HANDLE, "smaa.wgsl", Shader::from_wgsl);
291
292        // Load the two lookup textures. These are compressed textures in KTX2
293        // format.
294        #[cfg(feature = "smaa_luts")]
295        load_internal_binary_asset!(
296            app,
297            SMAA_AREA_LUT_TEXTURE_HANDLE,
298            "SMAAAreaLUT.ktx2",
299            |bytes, _: String| Image::from_buffer(
300                #[cfg(all(debug_assertions, feature = "dds"))]
301                "SMAAAreaLUT".to_owned(),
302                bytes,
303                bevy_image::ImageType::Format(bevy_image::ImageFormat::Ktx2),
304                bevy_image::CompressedImageFormats::NONE,
305                false,
306                bevy_image::ImageSampler::Default,
307                bevy_asset::RenderAssetUsages::RENDER_WORLD,
308            )
309            .expect("Failed to load SMAA area LUT")
310        );
311
312        #[cfg(feature = "smaa_luts")]
313        load_internal_binary_asset!(
314            app,
315            SMAA_SEARCH_LUT_TEXTURE_HANDLE,
316            "SMAASearchLUT.ktx2",
317            |bytes, _: String| Image::from_buffer(
318                #[cfg(all(debug_assertions, feature = "dds"))]
319                "SMAASearchLUT".to_owned(),
320                bytes,
321                bevy_image::ImageType::Format(bevy_image::ImageFormat::Ktx2),
322                bevy_image::CompressedImageFormats::NONE,
323                false,
324                bevy_image::ImageSampler::Default,
325                bevy_asset::RenderAssetUsages::RENDER_WORLD,
326            )
327            .expect("Failed to load SMAA search LUT")
328        );
329
330        #[cfg(not(feature = "smaa_luts"))]
331        app.world_mut()
332            .resource_mut::<bevy_asset::Assets<Image>>()
333            .insert(SMAA_AREA_LUT_TEXTURE_HANDLE.id(), lut_placeholder());
334
335        #[cfg(not(feature = "smaa_luts"))]
336        app.world_mut()
337            .resource_mut::<bevy_asset::Assets<Image>>()
338            .insert(SMAA_SEARCH_LUT_TEXTURE_HANDLE.id(), lut_placeholder());
339
340        app.add_plugins(ExtractComponentPlugin::<Smaa>::default())
341            .register_type::<Smaa>();
342
343        let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
344            return;
345        };
346
347        render_app
348            .init_resource::<SmaaSpecializedRenderPipelines>()
349            .init_resource::<SmaaInfoUniformBuffer>()
350            .add_systems(
351                Render,
352                (
353                    prepare_smaa_pipelines.in_set(RenderSet::Prepare),
354                    prepare_smaa_uniforms.in_set(RenderSet::PrepareResources),
355                    prepare_smaa_textures.in_set(RenderSet::PrepareResources),
356                    prepare_smaa_bind_groups.in_set(RenderSet::PrepareBindGroups),
357                ),
358            )
359            .add_render_graph_node::<ViewNodeRunner<SmaaNode>>(Core3d, Node3d::Smaa)
360            .add_render_graph_edges(
361                Core3d,
362                (
363                    Node3d::Tonemapping,
364                    Node3d::Smaa,
365                    Node3d::EndMainPassPostProcessing,
366                ),
367            )
368            .add_render_graph_node::<ViewNodeRunner<SmaaNode>>(Core2d, Node2d::Smaa)
369            .add_render_graph_edges(
370                Core2d,
371                (
372                    Node2d::Tonemapping,
373                    Node2d::Smaa,
374                    Node2d::EndMainPassPostProcessing,
375                ),
376            );
377    }
378
379    fn finish(&self, app: &mut App) {
380        if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
381            render_app.init_resource::<SmaaPipelines>();
382        }
383    }
384}
385
386impl FromWorld for SmaaPipelines {
387    fn from_world(world: &mut World) -> Self {
388        let render_device = world.resource::<RenderDevice>();
389
390        // Create the postprocess bind group layout (all passes, bind group 0).
391        let postprocess_bind_group_layout = render_device.create_bind_group_layout(
392            "SMAA postprocess bind group layout",
393            &BindGroupLayoutEntries::sequential(
394                ShaderStages::FRAGMENT,
395                (
396                    texture_2d(TextureSampleType::Float { filterable: true }),
397                    uniform_buffer::<SmaaInfoUniform>(true)
398                        .visibility(ShaderStages::VERTEX_FRAGMENT),
399                ),
400            ),
401        );
402
403        // Create the edge detection bind group layout (pass 1, bind group 1).
404        let edge_detection_bind_group_layout = render_device.create_bind_group_layout(
405            "SMAA edge detection bind group layout",
406            &BindGroupLayoutEntries::sequential(
407                ShaderStages::FRAGMENT,
408                (sampler(SamplerBindingType::Filtering),),
409            ),
410        );
411
412        // Create the blending weight calculation bind group layout (pass 2, bind group 1).
413        let blending_weight_calculation_bind_group_layout = render_device.create_bind_group_layout(
414            "SMAA blending weight calculation bind group layout",
415            &BindGroupLayoutEntries::sequential(
416                ShaderStages::FRAGMENT,
417                (
418                    texture_2d(TextureSampleType::Float { filterable: true }), // edges texture
419                    sampler(SamplerBindingType::Filtering),                    // edges sampler
420                    texture_2d(TextureSampleType::Float { filterable: true }), // search texture
421                    texture_2d(TextureSampleType::Float { filterable: true }), // area texture
422                ),
423            ),
424        );
425
426        // Create the neighborhood blending bind group layout (pass 3, bind group 1).
427        let neighborhood_blending_bind_group_layout = render_device.create_bind_group_layout(
428            "SMAA neighborhood blending bind group layout",
429            &BindGroupLayoutEntries::sequential(
430                ShaderStages::FRAGMENT,
431                (
432                    texture_2d(TextureSampleType::Float { filterable: true }),
433                    sampler(SamplerBindingType::Filtering),
434                ),
435            ),
436        );
437
438        SmaaPipelines {
439            edge_detection: SmaaEdgeDetectionPipeline {
440                postprocess_bind_group_layout: postprocess_bind_group_layout.clone(),
441                edge_detection_bind_group_layout,
442            },
443            blending_weight_calculation: SmaaBlendingWeightCalculationPipeline {
444                postprocess_bind_group_layout: postprocess_bind_group_layout.clone(),
445                blending_weight_calculation_bind_group_layout,
446            },
447            neighborhood_blending: SmaaNeighborhoodBlendingPipeline {
448                postprocess_bind_group_layout,
449                neighborhood_blending_bind_group_layout,
450            },
451        }
452    }
453}
454
455// Phase 1: edge detection.
456impl SpecializedRenderPipeline for SmaaEdgeDetectionPipeline {
457    type Key = SmaaPreset;
458
459    fn specialize(&self, preset: Self::Key) -> RenderPipelineDescriptor {
460        let shader_defs = vec!["SMAA_EDGE_DETECTION".into(), preset.shader_def()];
461
462        // We mark the pixels that we touched with a 1 so that the blending
463        // weight calculation (phase 2) will only consider those. This reduces
464        // the overhead of phase 2 considerably.
465        let stencil_face_state = StencilFaceState {
466            compare: CompareFunction::Always,
467            fail_op: StencilOperation::Replace,
468            depth_fail_op: StencilOperation::Replace,
469            pass_op: StencilOperation::Replace,
470        };
471
472        RenderPipelineDescriptor {
473            label: Some("SMAA edge detection".into()),
474            layout: vec![
475                self.postprocess_bind_group_layout.clone(),
476                self.edge_detection_bind_group_layout.clone(),
477            ],
478            vertex: VertexState {
479                shader: SMAA_SHADER_HANDLE,
480                shader_defs: shader_defs.clone(),
481                entry_point: "edge_detection_vertex_main".into(),
482                buffers: vec![],
483            },
484            fragment: Some(FragmentState {
485                shader: SMAA_SHADER_HANDLE,
486                shader_defs,
487                entry_point: "luma_edge_detection_fragment_main".into(),
488                targets: vec![Some(ColorTargetState {
489                    format: TextureFormat::Rg8Unorm,
490                    blend: None,
491                    write_mask: ColorWrites::ALL,
492                })],
493            }),
494            push_constant_ranges: vec![],
495            primitive: PrimitiveState::default(),
496            depth_stencil: Some(DepthStencilState {
497                format: TextureFormat::Stencil8,
498                depth_write_enabled: false,
499                depth_compare: CompareFunction::Always,
500                stencil: StencilState {
501                    front: stencil_face_state,
502                    back: stencil_face_state,
503                    read_mask: 1,
504                    write_mask: 1,
505                },
506                bias: default(),
507            }),
508            multisample: MultisampleState::default(),
509            zero_initialize_workgroup_memory: false,
510        }
511    }
512}
513
514// Phase 2: blending weight calculation.
515impl SpecializedRenderPipeline for SmaaBlendingWeightCalculationPipeline {
516    type Key = SmaaPreset;
517
518    fn specialize(&self, preset: Self::Key) -> RenderPipelineDescriptor {
519        let shader_defs = vec![
520            "SMAA_BLENDING_WEIGHT_CALCULATION".into(),
521            preset.shader_def(),
522        ];
523
524        // Only consider the pixels that were touched in phase 1.
525        let stencil_face_state = StencilFaceState {
526            compare: CompareFunction::Equal,
527            fail_op: StencilOperation::Keep,
528            depth_fail_op: StencilOperation::Keep,
529            pass_op: StencilOperation::Keep,
530        };
531
532        RenderPipelineDescriptor {
533            label: Some("SMAA blending weight calculation".into()),
534            layout: vec![
535                self.postprocess_bind_group_layout.clone(),
536                self.blending_weight_calculation_bind_group_layout.clone(),
537            ],
538            vertex: VertexState {
539                shader: SMAA_SHADER_HANDLE,
540                shader_defs: shader_defs.clone(),
541                entry_point: "blending_weight_calculation_vertex_main".into(),
542                buffers: vec![],
543            },
544            fragment: Some(FragmentState {
545                shader: SMAA_SHADER_HANDLE,
546                shader_defs,
547                entry_point: "blending_weight_calculation_fragment_main".into(),
548                targets: vec![Some(ColorTargetState {
549                    format: TextureFormat::Rgba8Unorm,
550                    blend: None,
551                    write_mask: ColorWrites::ALL,
552                })],
553            }),
554            push_constant_ranges: vec![],
555            primitive: PrimitiveState::default(),
556            depth_stencil: Some(DepthStencilState {
557                format: TextureFormat::Stencil8,
558                depth_write_enabled: false,
559                depth_compare: CompareFunction::Always,
560                stencil: StencilState {
561                    front: stencil_face_state,
562                    back: stencil_face_state,
563                    read_mask: 1,
564                    write_mask: 1,
565                },
566                bias: default(),
567            }),
568            multisample: MultisampleState::default(),
569            zero_initialize_workgroup_memory: false,
570        }
571    }
572}
573
574impl SpecializedRenderPipeline for SmaaNeighborhoodBlendingPipeline {
575    type Key = SmaaNeighborhoodBlendingPipelineKey;
576
577    fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
578        let shader_defs = vec!["SMAA_NEIGHBORHOOD_BLENDING".into(), key.preset.shader_def()];
579
580        RenderPipelineDescriptor {
581            label: Some("SMAA neighborhood blending".into()),
582            layout: vec![
583                self.postprocess_bind_group_layout.clone(),
584                self.neighborhood_blending_bind_group_layout.clone(),
585            ],
586            vertex: VertexState {
587                shader: SMAA_SHADER_HANDLE,
588                shader_defs: shader_defs.clone(),
589                entry_point: "neighborhood_blending_vertex_main".into(),
590                buffers: vec![],
591            },
592            fragment: Some(FragmentState {
593                shader: SMAA_SHADER_HANDLE,
594                shader_defs,
595                entry_point: "neighborhood_blending_fragment_main".into(),
596                targets: vec![Some(ColorTargetState {
597                    format: key.texture_format,
598                    blend: None,
599                    write_mask: ColorWrites::ALL,
600                })],
601            }),
602            push_constant_ranges: vec![],
603            primitive: PrimitiveState::default(),
604            depth_stencil: None,
605            multisample: MultisampleState::default(),
606            zero_initialize_workgroup_memory: false,
607        }
608    }
609}
610
611/// A system, part of the render app, that specializes the three pipelines
612/// needed for SMAA according to each view's SMAA settings.
613fn prepare_smaa_pipelines(
614    mut commands: Commands,
615    pipeline_cache: Res<PipelineCache>,
616    mut specialized_render_pipelines: ResMut<SmaaSpecializedRenderPipelines>,
617    smaa_pipelines: Res<SmaaPipelines>,
618    view_targets: Query<(Entity, &ExtractedView, &Smaa)>,
619) {
620    for (entity, view, smaa) in &view_targets {
621        let edge_detection_pipeline_id = specialized_render_pipelines.edge_detection.specialize(
622            &pipeline_cache,
623            &smaa_pipelines.edge_detection,
624            smaa.preset,
625        );
626
627        let blending_weight_calculation_pipeline_id = specialized_render_pipelines
628            .blending_weight_calculation
629            .specialize(
630                &pipeline_cache,
631                &smaa_pipelines.blending_weight_calculation,
632                smaa.preset,
633            );
634
635        let neighborhood_blending_pipeline_id = specialized_render_pipelines
636            .neighborhood_blending
637            .specialize(
638                &pipeline_cache,
639                &smaa_pipelines.neighborhood_blending,
640                SmaaNeighborhoodBlendingPipelineKey {
641                    texture_format: if view.hdr {
642                        ViewTarget::TEXTURE_FORMAT_HDR
643                    } else {
644                        TextureFormat::bevy_default()
645                    },
646                    preset: smaa.preset,
647                },
648            );
649
650        commands.entity(entity).insert(ViewSmaaPipelines {
651            edge_detection_pipeline_id,
652            blending_weight_calculation_pipeline_id,
653            neighborhood_blending_pipeline_id,
654        });
655    }
656}
657
658/// A system, part of the render app, that builds the [`SmaaInfoUniform`] data
659/// for each view with SMAA enabled and writes the resulting data to GPU memory.
660fn prepare_smaa_uniforms(
661    mut commands: Commands,
662    render_device: Res<RenderDevice>,
663    render_queue: Res<RenderQueue>,
664    view_targets: Query<(Entity, &ExtractedView), With<Smaa>>,
665    mut smaa_info_buffer: ResMut<SmaaInfoUniformBuffer>,
666) {
667    smaa_info_buffer.clear();
668    for (entity, view) in &view_targets {
669        let offset = smaa_info_buffer.push(&SmaaInfoUniform {
670            rt_metrics: vec4(
671                1.0 / view.viewport.z as f32,
672                1.0 / view.viewport.w as f32,
673                view.viewport.z as f32,
674                view.viewport.w as f32,
675            ),
676        });
677        commands
678            .entity(entity)
679            .insert(SmaaInfoUniformOffset(offset));
680    }
681
682    smaa_info_buffer.write_buffer(&render_device, &render_queue);
683}
684
685/// A system, part of the render app, that builds the intermediate textures for
686/// each view with SMAA enabled.
687///
688/// Phase 1 (edge detection) needs a two-channel RG texture and an 8-bit stencil
689/// texture; phase 2 (blend weight calculation) needs a four-channel RGBA
690/// texture.
691fn prepare_smaa_textures(
692    mut commands: Commands,
693    render_device: Res<RenderDevice>,
694    mut texture_cache: ResMut<TextureCache>,
695    view_targets: Query<(Entity, &ExtractedCamera), (With<ExtractedView>, With<Smaa>)>,
696) {
697    for (entity, camera) in &view_targets {
698        let Some(texture_size) = camera.physical_target_size else {
699            continue;
700        };
701
702        let texture_size = Extent3d {
703            width: texture_size.x,
704            height: texture_size.y,
705            depth_or_array_layers: 1,
706        };
707
708        // Create the two-channel RG texture for phase 1 (edge detection).
709        let edge_detection_color_texture = texture_cache.get(
710            &render_device,
711            TextureDescriptor {
712                label: Some("SMAA edge detection color texture"),
713                size: texture_size,
714                mip_level_count: 1,
715                sample_count: 1,
716                dimension: TextureDimension::D2,
717                format: TextureFormat::Rg8Unorm,
718                usage: TextureUsages::TEXTURE_BINDING | TextureUsages::RENDER_ATTACHMENT,
719                view_formats: &[],
720            },
721        );
722
723        // Create the stencil texture for phase 1 (edge detection).
724        let edge_detection_stencil_texture = texture_cache.get(
725            &render_device,
726            TextureDescriptor {
727                label: Some("SMAA edge detection stencil texture"),
728                size: texture_size,
729                mip_level_count: 1,
730                sample_count: 1,
731                dimension: TextureDimension::D2,
732                format: TextureFormat::Stencil8,
733                usage: TextureUsages::RENDER_ATTACHMENT,
734                view_formats: &[],
735            },
736        );
737
738        // Create the four-channel RGBA texture for phase 2 (blending weight
739        // calculation).
740        let blend_texture = texture_cache.get(
741            &render_device,
742            TextureDescriptor {
743                label: Some("SMAA blend texture"),
744                size: texture_size,
745                mip_level_count: 1,
746                sample_count: 1,
747                dimension: TextureDimension::D2,
748                format: TextureFormat::Rgba8Unorm,
749                usage: TextureUsages::TEXTURE_BINDING | TextureUsages::RENDER_ATTACHMENT,
750                view_formats: &[],
751            },
752        );
753
754        commands.entity(entity).insert(SmaaTextures {
755            edge_detection_color_texture,
756            edge_detection_stencil_texture,
757            blend_texture,
758        });
759    }
760}
761
762/// A system, part of the render app, that builds the SMAA bind groups for each
763/// view with SMAA enabled.
764fn prepare_smaa_bind_groups(
765    mut commands: Commands,
766    render_device: Res<RenderDevice>,
767    smaa_pipelines: Res<SmaaPipelines>,
768    images: Res<RenderAssets<GpuImage>>,
769    view_targets: Query<(Entity, &SmaaTextures), (With<ExtractedView>, With<Smaa>)>,
770) {
771    // Fetch the two lookup textures. These are bundled in this library.
772    let (Some(search_texture), Some(area_texture)) = (
773        images.get(&SMAA_SEARCH_LUT_TEXTURE_HANDLE),
774        images.get(&SMAA_AREA_LUT_TEXTURE_HANDLE),
775    ) else {
776        return;
777    };
778
779    for (entity, smaa_textures) in &view_targets {
780        // We use the same sampler settings for all textures, so we can build
781        // only one and reuse it.
782        let sampler = render_device.create_sampler(&SamplerDescriptor {
783            label: Some("SMAA sampler"),
784            address_mode_u: AddressMode::ClampToEdge,
785            address_mode_v: AddressMode::ClampToEdge,
786            address_mode_w: AddressMode::ClampToEdge,
787            mag_filter: FilterMode::Linear,
788            min_filter: FilterMode::Linear,
789            ..default()
790        });
791
792        commands.entity(entity).insert(SmaaBindGroups {
793            edge_detection_bind_group: render_device.create_bind_group(
794                Some("SMAA edge detection bind group"),
795                &smaa_pipelines
796                    .edge_detection
797                    .edge_detection_bind_group_layout,
798                &BindGroupEntries::sequential((&sampler,)),
799            ),
800            blending_weight_calculation_bind_group: render_device.create_bind_group(
801                Some("SMAA blending weight calculation bind group"),
802                &smaa_pipelines
803                    .blending_weight_calculation
804                    .blending_weight_calculation_bind_group_layout,
805                &BindGroupEntries::sequential((
806                    &smaa_textures.edge_detection_color_texture.default_view,
807                    &sampler,
808                    &search_texture.texture_view,
809                    &area_texture.texture_view,
810                )),
811            ),
812            neighborhood_blending_bind_group: render_device.create_bind_group(
813                Some("SMAA neighborhood blending bind group"),
814                &smaa_pipelines
815                    .neighborhood_blending
816                    .neighborhood_blending_bind_group_layout,
817                &BindGroupEntries::sequential((
818                    &smaa_textures.blend_texture.default_view,
819                    &sampler,
820                )),
821            ),
822        });
823    }
824}
825
826impl ViewNode for SmaaNode {
827    type ViewQuery = (
828        Read<ViewTarget>,
829        Read<ViewSmaaPipelines>,
830        Read<SmaaInfoUniformOffset>,
831        Read<SmaaTextures>,
832        Read<SmaaBindGroups>,
833    );
834
835    fn run<'w>(
836        &self,
837        _: &mut RenderGraphContext,
838        render_context: &mut RenderContext<'w>,
839        (
840            view_target,
841            view_pipelines,
842            view_smaa_uniform_offset,
843            smaa_textures,
844            view_smaa_bind_groups,
845        ): QueryItem<'w, Self::ViewQuery>,
846        world: &'w World,
847    ) -> Result<(), NodeRunError> {
848        let pipeline_cache = world.resource::<PipelineCache>();
849        let smaa_pipelines = world.resource::<SmaaPipelines>();
850        let smaa_info_uniform_buffer = world.resource::<SmaaInfoUniformBuffer>();
851
852        // Fetch the render pipelines.
853        let (
854            Some(edge_detection_pipeline),
855            Some(blending_weight_calculation_pipeline),
856            Some(neighborhood_blending_pipeline),
857        ) = (
858            pipeline_cache.get_render_pipeline(view_pipelines.edge_detection_pipeline_id),
859            pipeline_cache
860                .get_render_pipeline(view_pipelines.blending_weight_calculation_pipeline_id),
861            pipeline_cache.get_render_pipeline(view_pipelines.neighborhood_blending_pipeline_id),
862        )
863        else {
864            return Ok(());
865        };
866
867        // Fetch the framebuffer textures.
868        let postprocess = view_target.post_process_write();
869        let (source, destination) = (postprocess.source, postprocess.destination);
870
871        // Stage 1: Edge detection pass.
872        perform_edge_detection(
873            render_context,
874            smaa_pipelines,
875            smaa_textures,
876            view_smaa_bind_groups,
877            smaa_info_uniform_buffer,
878            view_smaa_uniform_offset,
879            edge_detection_pipeline,
880            source,
881        );
882
883        // Stage 2: Blending weight calculation pass.
884        perform_blending_weight_calculation(
885            render_context,
886            smaa_pipelines,
887            smaa_textures,
888            view_smaa_bind_groups,
889            smaa_info_uniform_buffer,
890            view_smaa_uniform_offset,
891            blending_weight_calculation_pipeline,
892            source,
893        );
894
895        // Stage 3: Neighborhood blending pass.
896        perform_neighborhood_blending(
897            render_context,
898            smaa_pipelines,
899            view_smaa_bind_groups,
900            smaa_info_uniform_buffer,
901            view_smaa_uniform_offset,
902            neighborhood_blending_pipeline,
903            source,
904            destination,
905        );
906
907        Ok(())
908    }
909}
910
911/// Performs edge detection (phase 1).
912///
913/// This runs as part of the [`SmaaNode`]. It reads from the source texture and
914/// writes to the two-channel RG edges texture. Additionally, it ensures that
915/// all pixels it didn't touch are stenciled out so that phase 2 won't have to
916/// examine them.
917#[allow(clippy::too_many_arguments)]
918fn perform_edge_detection(
919    render_context: &mut RenderContext,
920    smaa_pipelines: &SmaaPipelines,
921    smaa_textures: &SmaaTextures,
922    view_smaa_bind_groups: &SmaaBindGroups,
923    smaa_info_uniform_buffer: &SmaaInfoUniformBuffer,
924    view_smaa_uniform_offset: &SmaaInfoUniformOffset,
925    edge_detection_pipeline: &RenderPipeline,
926    source: &TextureView,
927) {
928    // Create the edge detection bind group.
929    let postprocess_bind_group = render_context.render_device().create_bind_group(
930        None,
931        &smaa_pipelines.edge_detection.postprocess_bind_group_layout,
932        &BindGroupEntries::sequential((source, &**smaa_info_uniform_buffer)),
933    );
934
935    // Create the edge detection pass descriptor.
936    let pass_descriptor = RenderPassDescriptor {
937        label: Some("SMAA edge detection pass"),
938        color_attachments: &[Some(RenderPassColorAttachment {
939            view: &smaa_textures.edge_detection_color_texture.default_view,
940            resolve_target: None,
941            ops: default(),
942        })],
943        depth_stencil_attachment: Some(RenderPassDepthStencilAttachment {
944            view: &smaa_textures.edge_detection_stencil_texture.default_view,
945            depth_ops: None,
946            stencil_ops: Some(Operations {
947                load: LoadOp::Clear(0),
948                store: StoreOp::Store,
949            }),
950        }),
951        timestamp_writes: None,
952        occlusion_query_set: None,
953    };
954
955    // Run the actual render pass.
956    let mut render_pass = render_context
957        .command_encoder()
958        .begin_render_pass(&pass_descriptor);
959    render_pass.set_pipeline(edge_detection_pipeline);
960    render_pass.set_bind_group(0, &postprocess_bind_group, &[**view_smaa_uniform_offset]);
961    render_pass.set_bind_group(1, &view_smaa_bind_groups.edge_detection_bind_group, &[]);
962    render_pass.set_stencil_reference(1);
963    render_pass.draw(0..3, 0..1);
964}
965
966/// Performs blending weight calculation (phase 2).
967///
968/// This runs as part of the [`SmaaNode`]. It reads the edges texture and writes
969/// to the blend weight texture, using the stencil buffer to avoid processing
970/// pixels it doesn't need to examine.
971#[allow(clippy::too_many_arguments)]
972fn perform_blending_weight_calculation(
973    render_context: &mut RenderContext,
974    smaa_pipelines: &SmaaPipelines,
975    smaa_textures: &SmaaTextures,
976    view_smaa_bind_groups: &SmaaBindGroups,
977    smaa_info_uniform_buffer: &SmaaInfoUniformBuffer,
978    view_smaa_uniform_offset: &SmaaInfoUniformOffset,
979    blending_weight_calculation_pipeline: &RenderPipeline,
980    source: &TextureView,
981) {
982    // Create the blending weight calculation bind group.
983    let postprocess_bind_group = render_context.render_device().create_bind_group(
984        None,
985        &smaa_pipelines
986            .blending_weight_calculation
987            .postprocess_bind_group_layout,
988        &BindGroupEntries::sequential((source, &**smaa_info_uniform_buffer)),
989    );
990
991    // Create the blending weight calculation pass descriptor.
992    let pass_descriptor = RenderPassDescriptor {
993        label: Some("SMAA blending weight calculation pass"),
994        color_attachments: &[Some(RenderPassColorAttachment {
995            view: &smaa_textures.blend_texture.default_view,
996            resolve_target: None,
997            ops: default(),
998        })],
999        depth_stencil_attachment: Some(RenderPassDepthStencilAttachment {
1000            view: &smaa_textures.edge_detection_stencil_texture.default_view,
1001            depth_ops: None,
1002            stencil_ops: Some(Operations {
1003                load: LoadOp::Load,
1004                store: StoreOp::Discard,
1005            }),
1006        }),
1007        timestamp_writes: None,
1008        occlusion_query_set: None,
1009    };
1010
1011    // Run the actual render pass.
1012    let mut render_pass = render_context
1013        .command_encoder()
1014        .begin_render_pass(&pass_descriptor);
1015    render_pass.set_pipeline(blending_weight_calculation_pipeline);
1016    render_pass.set_bind_group(0, &postprocess_bind_group, &[**view_smaa_uniform_offset]);
1017    render_pass.set_bind_group(
1018        1,
1019        &view_smaa_bind_groups.blending_weight_calculation_bind_group,
1020        &[],
1021    );
1022    render_pass.set_stencil_reference(1);
1023    render_pass.draw(0..3, 0..1);
1024}
1025
1026/// Performs blending weight calculation (phase 3).
1027///
1028/// This runs as part of the [`SmaaNode`]. It reads from the blend weight
1029/// texture. It's the only phase that writes to the postprocessing destination.
1030#[allow(clippy::too_many_arguments)]
1031fn perform_neighborhood_blending(
1032    render_context: &mut RenderContext,
1033    smaa_pipelines: &SmaaPipelines,
1034    view_smaa_bind_groups: &SmaaBindGroups,
1035    smaa_info_uniform_buffer: &SmaaInfoUniformBuffer,
1036    view_smaa_uniform_offset: &SmaaInfoUniformOffset,
1037    neighborhood_blending_pipeline: &RenderPipeline,
1038    source: &TextureView,
1039    destination: &TextureView,
1040) {
1041    let postprocess_bind_group = render_context.render_device().create_bind_group(
1042        None,
1043        &smaa_pipelines
1044            .neighborhood_blending
1045            .postprocess_bind_group_layout,
1046        &BindGroupEntries::sequential((source, &**smaa_info_uniform_buffer)),
1047    );
1048
1049    let pass_descriptor = RenderPassDescriptor {
1050        label: Some("SMAA neighborhood blending pass"),
1051        color_attachments: &[Some(RenderPassColorAttachment {
1052            view: destination,
1053            resolve_target: None,
1054            ops: default(),
1055        })],
1056        depth_stencil_attachment: None,
1057        timestamp_writes: None,
1058        occlusion_query_set: None,
1059    };
1060
1061    let mut neighborhood_blending_render_pass = render_context
1062        .command_encoder()
1063        .begin_render_pass(&pass_descriptor);
1064    neighborhood_blending_render_pass.set_pipeline(neighborhood_blending_pipeline);
1065    neighborhood_blending_render_pass.set_bind_group(
1066        0,
1067        &postprocess_bind_group,
1068        &[**view_smaa_uniform_offset],
1069    );
1070    neighborhood_blending_render_pass.set_bind_group(
1071        1,
1072        &view_smaa_bind_groups.neighborhood_blending_bind_group,
1073        &[],
1074    );
1075    neighborhood_blending_render_pass.draw(0..3, 0..1);
1076}
1077
1078impl SmaaPreset {
1079    /// Returns the `#define` in the shader corresponding to this quality
1080    /// preset.
1081    fn shader_def(&self) -> ShaderDefVal {
1082        match *self {
1083            SmaaPreset::Low => "SMAA_PRESET_LOW".into(),
1084            SmaaPreset::Medium => "SMAA_PRESET_MEDIUM".into(),
1085            SmaaPreset::High => "SMAA_PRESET_HIGH".into(),
1086            SmaaPreset::Ultra => "SMAA_PRESET_ULTRA".into(),
1087        }
1088    }
1089}