抗锯齿:MSAA / FXAA / TAA
3D 几何体的边缘在光栅化后呈锯齿状阶梯——这是采样不足导致的走样(aliasing)。抗锯齿(Anti-Aliasing,AA)的目标就是消除这些锯齿。Bevy 内置了三种方案,各有取舍。
MSAA:多重采样抗锯齿
MSAA(Multisample Anti-Aliasing)在硬件层面对每个像素的多个采样点进行光栅化判断。它对几何体边缘非常有效,但不影响着色器产生的走样(如高光闪烁)。
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::Off | 1 | 关闭 |
Msaa::Sample2 | 2 | 2 倍采样 |
Msaa::Sample4 | 4 | 4 倍采样(默认) |
Msaa::Sample8 | 8 | 8 倍采样 |
MSAA 的优势是只对几何边缘做多重采样,不会像全屏后处理那样增加着色开销。劣势是它对透明物体和着色器产生的锯齿无能为力,且高采样数会增加显存占用。
FXAA:快速近似抗锯齿
FXAA(Fast Approximate Anti-Aliasing)是一种图像空间的后处理算法——它在渲染完成后分析画面中的边缘并进行模糊。速度极快,但会模糊细节。
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 是一个组件,加到相机实体上:
use bevy::anti_alias::fxaa::Fxaa;
commands.spawn((
Camera3d::default(),
Fxaa::default(),
));Fxaa 组件的字段:
| 字段 | 类型 | 默认值 | 含义 |
|---|---|---|---|
enabled | bool | true | 是否启用 |
edge_threshold | Sensitivity | High | 边缘检测灵敏度 |
edge_threshold_min | Sensitivity | High | 暗部边缘检测灵敏度 |
Sensitivity 枚举有五档:Low、Medium、High、Ultra、Extreme。灵敏度越高,检测到的边缘越多,锯齿越少,但画面也越模糊。Ultra 和 Extreme 可能导致明显的细节丢失。
FXAA 的优势是开销极低且对所有类型的走样都有效。劣势是会模糊高频细节(如文字、细线),效果不如 MSAA 精确。
TAA:时间性抗锯齿
TAA(Temporal Anti-Aliasing)利用多帧累积——在每一帧对相机做微小抖动(jitter),然后将当前帧与历史帧混合。它能消除几乎所有类型的走样,包括着色器产生的闪烁。
use bevy::anti_alias::taa::TemporalAntiAliasing;
commands.spawn((
Camera3d::default(),
TemporalAntiAliasing::default(),
));TemporalAntiAliasing 组件只有一个字段:
| 字段 | 类型 | 默认值 | 含义 |
|---|---|---|---|
reset | bool | true | 重置历史帧。设为 true 后自动回到 false |
TAA 使用时需要额外组件:TemporalJitter、DepthPrepass、MotionVectorPrepass。在 Bevy 0.18 中,TemporalAntiAliasing 通过 #[require(...)] 自动声明这些依赖,插入时会自动添加。
TAA 的优势是效果最好——能处理几何走样、着色走样、透明物体走样。劣势是可能产生鬼影(ghosting)——快速移动的物体或相机切换时,历史帧来不及更新,残影会留在画面上。通过设置 reset: true 可以清空历史。
三种方案对比
| 特性 | MSAA | FXAA | TAA |
|---|---|---|---|
| 几何边缘 | 优秀 | 良好 | 优秀 |
| 着色走样 | 不处理 | 良好 | 优秀 |
| 透明物体 | 不处理 | 良好 | 优秀 |
| 性能开销 | 中等 | 极低 | 低 |
| 细节保留 | 最好 | 较差 | 好 |
| 鬼影 | 无 | 无 | 可能 |
| 适用场景 | 几何为主的场景 | 低端/性能优先 | 高画质需求 |
实际项目中,FXAA 适合对性能敏感的场景(如移动端),TAA 适合画质优先的桌面端,MSAA 适合几何体主导且无复杂着色器的场景。三者互斥——同一时刻只应启用一种。