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