bevy_core_pipeline/taa/
mod.rs

1#![expect(deprecated)]
2
3use crate::{
4    core_3d::graph::{Core3d, Node3d},
5    fullscreen_vertex_shader::fullscreen_shader_vertex_state,
6    prelude::Camera3d,
7    prepass::{DepthPrepass, MotionVectorPrepass, ViewPrepassTextures},
8};
9use bevy_app::{App, Plugin};
10use bevy_asset::{load_internal_asset, Handle};
11use bevy_core::FrameCount;
12use bevy_ecs::{
13    prelude::{Bundle, Component, Entity, ReflectComponent},
14    query::{QueryItem, With},
15    schedule::IntoSystemConfigs,
16    system::{Commands, Query, Res, ResMut, Resource},
17    world::{FromWorld, World},
18};
19use bevy_image::BevyDefault as _;
20use bevy_math::vec2;
21use bevy_reflect::{std_traits::ReflectDefault, Reflect};
22use bevy_render::{
23    camera::{ExtractedCamera, MipBias, TemporalJitter},
24    prelude::{Camera, Projection},
25    render_graph::{NodeRunError, RenderGraphApp, RenderGraphContext, ViewNode, ViewNodeRunner},
26    render_resource::{
27        binding_types::{sampler, texture_2d, texture_depth_2d},
28        BindGroupEntries, BindGroupLayout, BindGroupLayoutEntries, CachedRenderPipelineId,
29        ColorTargetState, ColorWrites, Extent3d, FilterMode, FragmentState, MultisampleState,
30        Operations, PipelineCache, PrimitiveState, RenderPassColorAttachment, RenderPassDescriptor,
31        RenderPipelineDescriptor, Sampler, SamplerBindingType, SamplerDescriptor, Shader,
32        ShaderStages, SpecializedRenderPipeline, SpecializedRenderPipelines, TextureDescriptor,
33        TextureDimension, TextureFormat, TextureSampleType, TextureUsages,
34    },
35    renderer::{RenderContext, RenderDevice},
36    sync_component::SyncComponentPlugin,
37    sync_world::RenderEntity,
38    texture::{CachedTexture, TextureCache},
39    view::{ExtractedView, Msaa, ViewTarget},
40    ExtractSchedule, MainWorld, Render, RenderApp, RenderSet,
41};
42use bevy_utils::tracing::warn;
43
44const TAA_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(656865235226276);
45
46/// Plugin for temporal anti-aliasing.
47///
48/// See [`TemporalAntiAliasing`] for more details.
49pub struct TemporalAntiAliasPlugin;
50
51impl Plugin for TemporalAntiAliasPlugin {
52    fn build(&self, app: &mut App) {
53        load_internal_asset!(app, TAA_SHADER_HANDLE, "taa.wgsl", Shader::from_wgsl);
54
55        app.register_type::<TemporalAntiAliasing>();
56
57        app.add_plugins(SyncComponentPlugin::<TemporalAntiAliasing>::default());
58
59        let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
60            return;
61        };
62        render_app
63            .init_resource::<SpecializedRenderPipelines<TaaPipeline>>()
64            .add_systems(ExtractSchedule, extract_taa_settings)
65            .add_systems(
66                Render,
67                (
68                    prepare_taa_jitter_and_mip_bias.in_set(RenderSet::ManageViews),
69                    prepare_taa_pipelines.in_set(RenderSet::Prepare),
70                    prepare_taa_history_textures.in_set(RenderSet::PrepareResources),
71                ),
72            )
73            .add_render_graph_node::<ViewNodeRunner<TemporalAntiAliasNode>>(Core3d, Node3d::Taa)
74            .add_render_graph_edges(
75                Core3d,
76                (
77                    Node3d::EndMainPass,
78                    Node3d::MotionBlur, // Running before TAA reduces edge artifacts and noise
79                    Node3d::Taa,
80                    Node3d::Bloom,
81                    Node3d::Tonemapping,
82                ),
83            );
84    }
85
86    fn finish(&self, app: &mut App) {
87        let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
88            return;
89        };
90
91        render_app.init_resource::<TaaPipeline>();
92    }
93}
94
95/// Bundle to apply temporal anti-aliasing.
96#[derive(Bundle, Default, Clone)]
97#[deprecated(
98    since = "0.15.0",
99    note = "Use the `TemporalAntiAlias` component instead. Inserting it will now also insert the other components required by it automatically."
100)]
101pub struct TemporalAntiAliasBundle {
102    pub settings: TemporalAntiAliasing,
103    pub jitter: TemporalJitter,
104    pub depth_prepass: DepthPrepass,
105    pub motion_vector_prepass: MotionVectorPrepass,
106}
107
108/// Component to apply temporal anti-aliasing to a 3D perspective camera.
109///
110/// Temporal anti-aliasing (TAA) is a form of image smoothing/filtering, like
111/// multisample anti-aliasing (MSAA), or fast approximate anti-aliasing (FXAA).
112/// TAA works by blending (averaging) each frame with the past few frames.
113///
114/// # Tradeoffs
115///
116/// Pros:
117/// * Filters more types of aliasing than MSAA, such as textures and singular bright pixels (specular aliasing)
118/// * Cost scales with screen/view resolution, unlike MSAA which scales with number of triangles
119/// * Greatly increases the quality of stochastic rendering techniques such as SSAO, certain shadow map sampling methods, etc
120///
121/// Cons:
122/// * Chance of "ghosting" - ghostly trails left behind moving objects
123/// * Thin geometry, lighting detail, or texture lines may flicker noisily or disappear
124///
125/// Because TAA blends past frames with the current frame, when the frames differ too much
126/// (such as with fast moving objects or camera cuts), ghosting artifacts may occur.
127///
128/// Artifacts tend to be reduced at higher framerates and rendering resolution.
129///
130/// # Usage Notes
131///
132/// The [`TemporalAntiAliasPlugin`] must be added to your app.
133/// Any camera with this component must also disable [`Msaa`] by setting it to [`Msaa::Off`].
134///
135/// [Currently](https://github.com/bevyengine/bevy/issues/8423), TAA cannot be used with [`bevy_render::camera::OrthographicProjection`].
136///
137/// TAA also does not work well with alpha-blended meshes, as it requires depth writing to determine motion.
138///
139/// It is very important that correct motion vectors are written for everything on screen.
140/// Failure to do so will lead to ghosting artifacts. For instance, if particle effects
141/// are added using a third party library, the library must either:
142///
143/// 1. Write particle motion vectors to the motion vectors prepass texture
144/// 2. Render particles after TAA
145///
146/// If no [`MipBias`] component is attached to the camera, TAA will add a `MipBias(-1.0)` component.
147#[derive(Component, Reflect, Clone)]
148#[reflect(Component, Default)]
149#[require(TemporalJitter, DepthPrepass, MotionVectorPrepass)]
150#[doc(alias = "Taa")]
151pub struct TemporalAntiAliasing {
152    /// Set to true to delete the saved temporal history (past frames).
153    ///
154    /// Useful for preventing ghosting when the history is no longer
155    /// representative of the current frame, such as in sudden camera cuts.
156    ///
157    /// After setting this to true, it will automatically be toggled
158    /// back to false at the end of the frame.
159    pub reset: bool,
160}
161
162#[deprecated(since = "0.15.0", note = "Renamed to `TemporalAntiAliasing`")]
163pub type TemporalAntiAliasSettings = TemporalAntiAliasing;
164
165impl Default for TemporalAntiAliasing {
166    fn default() -> Self {
167        Self { reset: true }
168    }
169}
170
171/// Render [`bevy_render::render_graph::Node`] used by temporal anti-aliasing.
172#[derive(Default)]
173pub struct TemporalAntiAliasNode;
174
175impl ViewNode for TemporalAntiAliasNode {
176    type ViewQuery = (
177        &'static ExtractedCamera,
178        &'static ViewTarget,
179        &'static TemporalAntiAliasHistoryTextures,
180        &'static ViewPrepassTextures,
181        &'static TemporalAntiAliasPipelineId,
182        &'static Msaa,
183    );
184
185    fn run(
186        &self,
187        _graph: &mut RenderGraphContext,
188        render_context: &mut RenderContext,
189        (camera, view_target, taa_history_textures, prepass_textures, taa_pipeline_id, msaa): QueryItem<
190            Self::ViewQuery,
191        >,
192        world: &World,
193    ) -> Result<(), NodeRunError> {
194        if *msaa != Msaa::Off {
195            warn!("Temporal anti-aliasing requires MSAA to be disabled");
196            return Ok(());
197        }
198
199        let (Some(pipelines), Some(pipeline_cache)) = (
200            world.get_resource::<TaaPipeline>(),
201            world.get_resource::<PipelineCache>(),
202        ) else {
203            return Ok(());
204        };
205        let (Some(taa_pipeline), Some(prepass_motion_vectors_texture), Some(prepass_depth_texture)) = (
206            pipeline_cache.get_render_pipeline(taa_pipeline_id.0),
207            &prepass_textures.motion_vectors,
208            &prepass_textures.depth,
209        ) else {
210            return Ok(());
211        };
212        let view_target = view_target.post_process_write();
213
214        let taa_bind_group = render_context.render_device().create_bind_group(
215            "taa_bind_group",
216            &pipelines.taa_bind_group_layout,
217            &BindGroupEntries::sequential((
218                view_target.source,
219                &taa_history_textures.read.default_view,
220                &prepass_motion_vectors_texture.texture.default_view,
221                &prepass_depth_texture.texture.default_view,
222                &pipelines.nearest_sampler,
223                &pipelines.linear_sampler,
224            )),
225        );
226
227        {
228            let mut taa_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor {
229                label: Some("taa_pass"),
230                color_attachments: &[
231                    Some(RenderPassColorAttachment {
232                        view: view_target.destination,
233                        resolve_target: None,
234                        ops: Operations::default(),
235                    }),
236                    Some(RenderPassColorAttachment {
237                        view: &taa_history_textures.write.default_view,
238                        resolve_target: None,
239                        ops: Operations::default(),
240                    }),
241                ],
242                depth_stencil_attachment: None,
243                timestamp_writes: None,
244                occlusion_query_set: None,
245            });
246            taa_pass.set_render_pipeline(taa_pipeline);
247            taa_pass.set_bind_group(0, &taa_bind_group, &[]);
248            if let Some(viewport) = camera.viewport.as_ref() {
249                taa_pass.set_camera_viewport(viewport);
250            }
251            taa_pass.draw(0..3, 0..1);
252        }
253
254        Ok(())
255    }
256}
257
258#[derive(Resource)]
259struct TaaPipeline {
260    taa_bind_group_layout: BindGroupLayout,
261    nearest_sampler: Sampler,
262    linear_sampler: Sampler,
263}
264
265impl FromWorld for TaaPipeline {
266    fn from_world(world: &mut World) -> Self {
267        let render_device = world.resource::<RenderDevice>();
268
269        let nearest_sampler = render_device.create_sampler(&SamplerDescriptor {
270            label: Some("taa_nearest_sampler"),
271            mag_filter: FilterMode::Nearest,
272            min_filter: FilterMode::Nearest,
273            ..SamplerDescriptor::default()
274        });
275        let linear_sampler = render_device.create_sampler(&SamplerDescriptor {
276            label: Some("taa_linear_sampler"),
277            mag_filter: FilterMode::Linear,
278            min_filter: FilterMode::Linear,
279            ..SamplerDescriptor::default()
280        });
281
282        let taa_bind_group_layout = render_device.create_bind_group_layout(
283            "taa_bind_group_layout",
284            &BindGroupLayoutEntries::sequential(
285                ShaderStages::FRAGMENT,
286                (
287                    // View target (read)
288                    texture_2d(TextureSampleType::Float { filterable: true }),
289                    // TAA History (read)
290                    texture_2d(TextureSampleType::Float { filterable: true }),
291                    // Motion Vectors
292                    texture_2d(TextureSampleType::Float { filterable: true }),
293                    // Depth
294                    texture_depth_2d(),
295                    // Nearest sampler
296                    sampler(SamplerBindingType::NonFiltering),
297                    // Linear sampler
298                    sampler(SamplerBindingType::Filtering),
299                ),
300            ),
301        );
302
303        TaaPipeline {
304            taa_bind_group_layout,
305            nearest_sampler,
306            linear_sampler,
307        }
308    }
309}
310
311#[derive(PartialEq, Eq, Hash, Clone)]
312struct TaaPipelineKey {
313    hdr: bool,
314    reset: bool,
315}
316
317impl SpecializedRenderPipeline for TaaPipeline {
318    type Key = TaaPipelineKey;
319
320    fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
321        let mut shader_defs = vec![];
322
323        let format = if key.hdr {
324            shader_defs.push("TONEMAP".into());
325            ViewTarget::TEXTURE_FORMAT_HDR
326        } else {
327            TextureFormat::bevy_default()
328        };
329
330        if key.reset {
331            shader_defs.push("RESET".into());
332        }
333
334        RenderPipelineDescriptor {
335            label: Some("taa_pipeline".into()),
336            layout: vec![self.taa_bind_group_layout.clone()],
337            vertex: fullscreen_shader_vertex_state(),
338            fragment: Some(FragmentState {
339                shader: TAA_SHADER_HANDLE,
340                shader_defs,
341                entry_point: "taa".into(),
342                targets: vec![
343                    Some(ColorTargetState {
344                        format,
345                        blend: None,
346                        write_mask: ColorWrites::ALL,
347                    }),
348                    Some(ColorTargetState {
349                        format,
350                        blend: None,
351                        write_mask: ColorWrites::ALL,
352                    }),
353                ],
354            }),
355            primitive: PrimitiveState::default(),
356            depth_stencil: None,
357            multisample: MultisampleState::default(),
358            push_constant_ranges: Vec::new(),
359            zero_initialize_workgroup_memory: false,
360        }
361    }
362}
363
364fn extract_taa_settings(mut commands: Commands, mut main_world: ResMut<MainWorld>) {
365    let mut cameras_3d = main_world.query_filtered::<(
366        RenderEntity,
367        &Camera,
368        &Projection,
369        &mut TemporalAntiAliasing,
370    ), (
371        With<Camera3d>,
372        With<TemporalJitter>,
373        With<DepthPrepass>,
374        With<MotionVectorPrepass>,
375    )>();
376
377    for (entity, camera, camera_projection, mut taa_settings) in
378        cameras_3d.iter_mut(&mut main_world)
379    {
380        let has_perspective_projection = matches!(camera_projection, Projection::Perspective(_));
381        let mut entity_commands = commands
382            .get_entity(entity)
383            .expect("Camera entity wasn't synced.");
384        if camera.is_active && has_perspective_projection {
385            entity_commands.insert(taa_settings.clone());
386            taa_settings.reset = false;
387        } else {
388            // TODO: needs better strategy for cleaning up
389            entity_commands.remove::<(
390                TemporalAntiAliasing,
391                // components added in prepare systems (because `TemporalAntiAliasNode` does not query extracted components)
392                TemporalAntiAliasHistoryTextures,
393                TemporalAntiAliasPipelineId,
394            )>();
395        }
396    }
397}
398
399fn prepare_taa_jitter_and_mip_bias(
400    frame_count: Res<FrameCount>,
401    mut query: Query<(Entity, &mut TemporalJitter, Option<&MipBias>), With<TemporalAntiAliasing>>,
402    mut commands: Commands,
403) {
404    // Halton sequence (2, 3) - 0.5, skipping i = 0
405    let halton_sequence = [
406        vec2(0.0, -0.16666666),
407        vec2(-0.25, 0.16666669),
408        vec2(0.25, -0.3888889),
409        vec2(-0.375, -0.055555552),
410        vec2(0.125, 0.2777778),
411        vec2(-0.125, -0.2777778),
412        vec2(0.375, 0.055555582),
413        vec2(-0.4375, 0.3888889),
414    ];
415
416    let offset = halton_sequence[frame_count.0 as usize % halton_sequence.len()];
417
418    for (entity, mut jitter, mip_bias) in &mut query {
419        jitter.offset = offset;
420
421        if mip_bias.is_none() {
422            commands.entity(entity).insert(MipBias(-1.0));
423        }
424    }
425}
426
427#[derive(Component)]
428pub struct TemporalAntiAliasHistoryTextures {
429    write: CachedTexture,
430    read: CachedTexture,
431}
432
433fn prepare_taa_history_textures(
434    mut commands: Commands,
435    mut texture_cache: ResMut<TextureCache>,
436    render_device: Res<RenderDevice>,
437    frame_count: Res<FrameCount>,
438    views: Query<(Entity, &ExtractedCamera, &ExtractedView), With<TemporalAntiAliasing>>,
439) {
440    for (entity, camera, view) in &views {
441        if let Some(physical_target_size) = camera.physical_target_size {
442            let mut texture_descriptor = TextureDescriptor {
443                label: None,
444                size: Extent3d {
445                    depth_or_array_layers: 1,
446                    width: physical_target_size.x,
447                    height: physical_target_size.y,
448                },
449                mip_level_count: 1,
450                sample_count: 1,
451                dimension: TextureDimension::D2,
452                format: if view.hdr {
453                    ViewTarget::TEXTURE_FORMAT_HDR
454                } else {
455                    TextureFormat::bevy_default()
456                },
457                usage: TextureUsages::TEXTURE_BINDING | TextureUsages::RENDER_ATTACHMENT,
458                view_formats: &[],
459            };
460
461            texture_descriptor.label = Some("taa_history_1_texture");
462            let history_1_texture = texture_cache.get(&render_device, texture_descriptor.clone());
463
464            texture_descriptor.label = Some("taa_history_2_texture");
465            let history_2_texture = texture_cache.get(&render_device, texture_descriptor);
466
467            let textures = if frame_count.0 % 2 == 0 {
468                TemporalAntiAliasHistoryTextures {
469                    write: history_1_texture,
470                    read: history_2_texture,
471                }
472            } else {
473                TemporalAntiAliasHistoryTextures {
474                    write: history_2_texture,
475                    read: history_1_texture,
476                }
477            };
478
479            commands.entity(entity).insert(textures);
480        }
481    }
482}
483
484#[derive(Component)]
485pub struct TemporalAntiAliasPipelineId(CachedRenderPipelineId);
486
487fn prepare_taa_pipelines(
488    mut commands: Commands,
489    pipeline_cache: Res<PipelineCache>,
490    mut pipelines: ResMut<SpecializedRenderPipelines<TaaPipeline>>,
491    pipeline: Res<TaaPipeline>,
492    views: Query<(Entity, &ExtractedView, &TemporalAntiAliasing)>,
493) {
494    for (entity, view, taa_settings) in &views {
495        let mut pipeline_key = TaaPipelineKey {
496            hdr: view.hdr,
497            reset: taa_settings.reset,
498        };
499        let pipeline_id = pipelines.specialize(&pipeline_cache, &pipeline, pipeline_key.clone());
500
501        // Prepare non-reset pipeline anyways - it will be necessary next frame
502        if pipeline_key.reset {
503            pipeline_key.reset = false;
504            pipelines.specialize(&pipeline_cache, &pipeline, pipeline_key);
505        }
506
507        commands
508            .entity(entity)
509            .insert(TemporalAntiAliasPipelineId(pipeline_id));
510    }
511}