Skip to content

抗锯齿:MSAA / FXAA / TAA

3D 几何体的边缘在光栅化后呈锯齿状阶梯——这是采样不足导致的走样(aliasing)。抗锯齿(Anti-Aliasing,AA)的目标就是消除这些锯齿。Bevy 内置了三种方案,各有取舍。

MSAA:多重采样抗锯齿

MSAA(Multisample Anti-Aliasing)在硬件层面对每个像素的多个采样点进行光栅化判断。它对几何体边缘非常有效,但不影响着色器产生的走样(如高光闪烁)。

rust
use bevy::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_systems(Startup, setup)
        .add_systems(Update, switch_msaa)
        .run();
}

fn setup(
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<StandardMaterial>>,
) {
    commands.spawn((
        Camera3d::default(),
        Msaa::Sample4,
        Transform::from_xyz(2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
    ));

    commands.spawn((
        DirectionalLight {
            illuminance: 15000.0,
            ..default()
        },
        Transform::from_rotation(Quat::from_euler(EulerRot::ZYX, 0.0, -0.5, -0.5)),
    ));

    let cube = meshes.add(Cuboid::new(1.0, 1.0, 1.0));
    let mat = materials.add(Color::srgb(0.6, 0.7, 0.8));
    for i in 0..5 {
        let angle = i as f32 * 0.3;
        commands.spawn((
            Mesh3d(cube.clone()),
            MeshMaterial3d(mat.clone()),
            Transform::from_xyz(i as f32 * 1.5 - 3.0, 0.5, -angle * 2.0)
                .with_rotation(Quat::from_rotation_y(angle)),
        ));
    }

    commands.spawn((
        Mesh3d(meshes.add(Plane3d::default().mesh().size(20.0, 20.0))),
        MeshMaterial3d(materials.add(Color::srgb(0.3, 0.3, 0.3))),
    ));

    commands.spawn((
        Text::default(),
        Node {
            position_type: PositionType::Absolute,
            top: px(12),
            left: px(12),
            ..default()
        },
    ));
}

fn switch_msaa(mut msaa: Single<&mut Msaa>, mut text: Single<&mut Text>, keys: Res<ButtonInput<KeyCode>>) {
    if keys.just_pressed(KeyCode::Digit1) {
        **msaa = Msaa::Off;
    }
    if keys.just_pressed(KeyCode::Digit2) {
        **msaa = Msaa::Sample2;
    }
    if keys.just_pressed(KeyCode::Digit3) {
        **msaa = Msaa::Sample4;
    }
    if keys.just_pressed(KeyCode::Digit4) {
        **msaa = Msaa::Sample8;
    }

    let label = match **msaa {
        Msaa::Off => "Off (1x)",
        Msaa::Sample2 => "2x",
        Msaa::Sample4 => "4x",
        Msaa::Sample8 => "8x",
    };
    text.0 = format!("MSAA: {label}\n(1=Off, 2=2x, 3=4x, 4=8x)");
}

Listing 26-5:MSAA 采样数切换——对比不同级别的边缘平滑度

Msaa 是一个枚举组件,挂在相机实体上:

变体采样数说明
Msaa::Off1关闭
Msaa::Sample222 倍采样
Msaa::Sample444 倍采样(默认)
Msaa::Sample888 倍采样

MSAA 的优势是只对几何边缘做多重采样,不会像全屏后处理那样增加着色开销。劣势是它对透明物体和着色器产生的锯齿无能为力,且高采样数会增加显存占用。

FXAA:快速近似抗锯齿

FXAA(Fast Approximate Anti-Aliasing)是一种图像空间的后处理算法——它在渲染完成后分析画面中的边缘并进行模糊。速度极快,但会模糊细节。

rust
use bevy::{
    anti_alias::{fxaa::Fxaa, taa::TemporalAntiAliasing},
    core_pipeline::prepass::{DepthPrepass, MotionVectorPrepass},
    prelude::*,
    camera::Hdr,
    render::camera::TemporalJitter,
};

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_systems(Startup, setup)
        .add_systems(Update, switch_aa)
        .run();
}

#[derive(Component)]
struct AaLabel;

fn setup(
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<StandardMaterial>>,
) {
    commands.spawn((
        Camera3d::default(),
        Hdr,
        Transform::from_xyz(2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
        Msaa::Off,
        Fxaa::default(),
    ));

    commands.spawn((
        DirectionalLight {
            illuminance: 15000.0,
            ..default()
        },
        Transform::from_rotation(Quat::from_euler(EulerRot::ZYX, 0.0, -0.5, -0.5)),
    ));

    let mat = materials.add(Color::srgb(0.6, 0.7, 0.8));
    let cube = meshes.add(Cuboid::new(0.5, 0.5, 0.5));
    for i in 0..5 {
        let angle = i as f32 * 0.4;
        commands.spawn((
            Mesh3d(cube.clone()),
            MeshMaterial3d(mat.clone()),
            Transform::from_xyz(i as f32 * 1.0 - 2.0, 0.25, -angle * 1.5)
                .with_rotation(Quat::from_rotation_y(angle)),
        ));
    }
    commands.spawn((
        Mesh3d(meshes.add(Plane3d::default().mesh().size(20.0, 20.0))),
        MeshMaterial3d(materials.add(Color::srgb(0.3, 0.3, 0.3))),
    ));

    commands.spawn((
        Text::default(),
        AaLabel,
        Node {
            position_type: PositionType::Absolute,
            top: px(12),
            left: px(12),
            ..default()
        },
    ));
}

fn switch_aa(
    camera: Single<Entity, With<Camera>>,
    mut text: Single<&mut Text, With<AaLabel>>,
    keys: Res<ButtonInput<KeyCode>>,
    mut commands: Commands,
) {
    let entity = camera.into_inner();
    let mut cam = commands.entity(entity);

    if keys.just_pressed(KeyCode::Digit1) {
        cam.remove::<Fxaa>()
            .remove::<(TemporalAntiAliasing, TemporalJitter, DepthPrepass, MotionVectorPrepass)>()
            .insert(Msaa::Off);
    }
    if keys.just_pressed(KeyCode::Digit2) {
        cam.remove::<Fxaa>()
            .remove::<(TemporalAntiAliasing, TemporalJitter, DepthPrepass, MotionVectorPrepass)>()
            .insert(Msaa::Sample4);
    }
    if keys.just_pressed(KeyCode::Digit3) {
        cam.remove::<(TemporalAntiAliasing, TemporalJitter, DepthPrepass, MotionVectorPrepass)>()
            .remove::<Msaa>()
            .insert(Fxaa::default());
    }
    if keys.just_pressed(KeyCode::Digit4) {
        cam.remove::<Fxaa>()
            .remove::<Msaa>()
            .insert((
                TemporalAntiAliasing::default(),
                TemporalJitter::default(),
                DepthPrepass,
                MotionVectorPrepass,
            ));
    }

    text.0 = "Anti-Aliasing\n\
        (1) None\n\
        (2) MSAA 4x\n\
        (3) FXAA\n\
        (4) TAA"
        .into();
}

Listing 26-6:三种抗锯齿方案的切换对比

FXAA 是一个组件,加到相机实体上:

rust
use bevy::anti_alias::fxaa::Fxaa;

commands.spawn((
    Camera3d::default(),
    Fxaa::default(),
));

Fxaa 组件的字段:

字段类型默认值含义
enabledbooltrue是否启用
edge_thresholdSensitivityHigh边缘检测灵敏度
edge_threshold_minSensitivityHigh暗部边缘检测灵敏度

Sensitivity 枚举有五档:LowMediumHighUltraExtreme。灵敏度越高,检测到的边缘越多,锯齿越少,但画面也越模糊。UltraExtreme 可能导致明显的细节丢失。

FXAA 的优势是开销极低且对所有类型的走样都有效。劣势是会模糊高频细节(如文字、细线),效果不如 MSAA 精确。

TAA:时间性抗锯齿

TAA(Temporal Anti-Aliasing)利用多帧累积——在每一帧对相机做微小抖动(jitter),然后将当前帧与历史帧混合。它能消除几乎所有类型的走样,包括着色器产生的闪烁。

rust
use bevy::anti_alias::taa::TemporalAntiAliasing;

commands.spawn((
    Camera3d::default(),
    TemporalAntiAliasing::default(),
));

TemporalAntiAliasing 组件只有一个字段:

字段类型默认值含义
resetbooltrue重置历史帧。设为 true 后自动回到 false

TAA 使用时需要额外组件:TemporalJitterDepthPrepassMotionVectorPrepass。在 Bevy 0.18 中,TemporalAntiAliasing 通过 #[require(...)] 自动声明这些依赖,插入时会自动添加。

TAA 的优势是效果最好——能处理几何走样、着色走样、透明物体走样。劣势是可能产生鬼影(ghosting)——快速移动的物体或相机切换时,历史帧来不及更新,残影会留在画面上。通过设置 reset: true 可以清空历史。

三种方案对比

特性MSAAFXAATAA
几何边缘优秀良好优秀
着色走样不处理良好优秀
透明物体不处理良好优秀
性能开销中等极低
细节保留最好较差
鬼影可能
适用场景几何为主的场景低端/性能优先高画质需求

实际项目中,FXAA 适合对性能敏感的场景(如移动端),TAA 适合画质优先的桌面端,MSAA 适合几何体主导且无复杂着色器的场景。三者互斥——同一时刻只应启用一种。