画质开关面板
把前几节学到的后处理效果组装成一个统一的画质控制面板。这个示例展示了如何在运行时动态添加、移除组件来切换效果——这也是 Bevy ECS 的典型模式:效果即组件,开关即插入/移除。
rust
use bevy::{
anti_alias::fxaa::Fxaa,
core_pipeline::tonemapping::Tonemapping,
post_process::{
bloom::Bloom,
effect_stack::ChromaticAberration,
},
prelude::*,
render::view::Msaa,
};
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.init_resource::<QualityPreset>()
.add_systems(Startup, setup)
.add_systems(Update, (handle_input, apply_preset, update_ui))
.run();
}
#[derive(Resource)]
struct QualityPreset {
bloom: bool,
chromatic_aberration: bool,
msaa: Msaa,
fxaa: bool,
tonemapping: Tonemapping,
}
impl Default for QualityPreset {
fn default() -> Self {
Self {
bloom: true,
chromatic_aberration: false,
msaa: Msaa::Sample4,
fxaa: false,
tonemapping: Tonemapping::TonyMcMapface,
}
}
}
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
commands.spawn((
Camera3d::default(),
Tonemapping::TonyMcMapface,
Transform::from_xyz(2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
Bloom::NATURAL,
));
commands.spawn((
DirectionalLight {
illuminance: 15000.0,
..default()
},
Transform::from_rotation(Quat::from_euler(EulerRot::ZYX, 0.0, -0.5, -0.5)),
));
let emissive = materials.add(StandardMaterial {
emissive: LinearRgba::rgb(8.0, 2.0, 1.0),
..default()
});
let mat = materials.add(Color::srgb(0.5, 0.6, 0.7));
let sphere = meshes.add(Sphere::new(0.5));
commands.spawn((
Mesh3d(sphere.clone()),
MeshMaterial3d(emissive),
Transform::from_xyz(-1.5, 0.5, 0.0),
));
commands.spawn((
Mesh3d(sphere),
MeshMaterial3d(mat),
Transform::from_xyz(1.5, 0.5, 0.0),
));
commands.spawn((
Mesh3d(meshes.add(Plane3d::default().mesh().size(20.0, 20.0))),
MeshMaterial3d(materials.add(Color::srgb(0.15, 0.15, 0.15))),
));
commands.spawn((
Text::default(),
Node {
position_type: PositionType::Absolute,
top: px(12),
left: px(12),
..default()
},
));
}
fn handle_input(
keys: Res<ButtonInput<KeyCode>>,
mut preset: ResMut<QualityPreset>,
) {
if keys.just_pressed(KeyCode::KeyB) {
preset.bloom = !preset.bloom;
}
if keys.just_pressed(KeyCode::KeyC) {
preset.chromatic_aberration = !preset.chromatic_aberration;
}
if keys.just_pressed(KeyCode::KeyF) {
preset.fxaa = !preset.fxaa;
}
if keys.just_pressed(KeyCode::Digit1) {
preset.msaa = Msaa::Off;
}
if keys.just_pressed(KeyCode::Digit2) {
preset.msaa = Msaa::Sample2;
}
if keys.just_pressed(KeyCode::Digit3) {
preset.msaa = Msaa::Sample4;
}
if keys.just_pressed(KeyCode::Digit4) {
preset.msaa = Msaa::Sample8;
}
if keys.just_pressed(KeyCode::KeyT) {
preset.tonemapping = match preset.tonemapping {
Tonemapping::None => Tonemapping::Reinhard,
Tonemapping::Reinhard => Tonemapping::AcesFitted,
Tonemapping::AcesFitted => Tonemapping::TonyMcMapface,
Tonemapping::TonyMcMapface => Tonemapping::AgX,
_ => Tonemapping::None,
};
}
}
fn apply_preset(
preset: Res<QualityPreset>,
camera: Single<(
Entity,
Option<&Bloom>,
Option<&ChromaticAberration>,
Option<&Fxaa>,
&mut Msaa,
)>,
mut commands: Commands,
) {
if !preset.is_changed() {
return;
}
let (entity, has_bloom, has_ca, has_fxaa, mut msaa) = camera.into_inner();
let mut cam = commands.entity(entity);
if preset.bloom && has_bloom.is_none() {
cam.insert(Bloom::NATURAL);
} else if !preset.bloom && has_bloom.is_some() {
cam.remove::<Bloom>();
}
if preset.chromatic_aberration && has_ca.is_none() {
cam.insert(ChromaticAberration::default());
} else if !preset.chromatic_aberration && has_ca.is_some() {
cam.remove::<ChromaticAberration>();
}
if preset.fxaa && has_fxaa.is_none() {
cam.insert(Fxaa::default());
} else if !preset.fxaa && has_fxaa.is_some() {
cam.remove::<Fxaa>();
}
*msaa = preset.msaa;
cam.insert(preset.tonemapping);
}
fn update_ui(preset: Res<QualityPreset>, mut text: Single<&mut Text>) {
let msaa_label = match preset.msaa {
Msaa::Off => "Off",
Msaa::Sample2 => "2x",
Msaa::Sample4 => "4x",
Msaa::Sample8 => "8x",
};
let tm_name = match preset.tonemapping {
Tonemapping::None => "None",
Tonemapping::Reinhard => "Reinhard",
Tonemapping::ReinhardLuminance => "ReinhardLuminance",
Tonemapping::AcesFitted => "AcesFitted",
Tonemapping::AgX => "AgX",
Tonemapping::SomewhatBoringDisplayTransform => "SBDT",
Tonemapping::TonyMcMapface => "TonyMcMapface",
Tonemapping::BlenderFilmic => "BlenderFilmic",
Tonemapping::KhronosPbrNeutral => "KhronosPbrNeutral",
};
text.0 = format!(
"画质控制面板\n\
──────────────\n\
(B) Bloom: {}\n\
(C) 色散: {}\n\
(F) FXAA: {}\n\
(1-4) MSAA: {}\n\
(T) Tonemapping: {}",
on_off(preset.bloom),
on_off(preset.chromatic_aberration),
on_off(preset.fxaa),
msaa_label,
tm_name,
);
}
fn on_off(v: bool) -> &'static str {
if v { "On" } else { "Off" }
}Listing 26-8:画质控制面板——Bloom、色散、FXAA、MSAA、Tonemapping 的统一开关
核心模式:is_changed 守卫
apply_preset 函数使用 preset.is_changed() 避免每帧重复操作。只有当用户按键修改了 QualityPreset 资源时,才执行组件的插入和移除。这是 Bevy 中常见的性能优化——Res<T>::is_changed() 在资源被修改后的第一帧返回 true,之后自动回到 false。
组件插入与移除
后处理效果的开关本质上是组件的插入和移除:
rust
// 开启 Bloom
commands.entity(camera).insert(Bloom::NATURAL);
// 关闭 Bloom
commands.entity(camera).remove::<Bloom>();每种效果组件的插入和移除都是对称的。Bloom 通过 #[require(Hdr)] 自动处理 HDR 依赖;TemporalAntiAliasing 通过 #[require(...)] 自动处理 jitter 和 prepass 依赖。手动管理这些依赖会很繁琐,但 Bevy 的 require 机制帮我们省去了这一步。
资源 vs 组件
画质设置存放在 QualityPreset 资源中,而不是直接操作组件。这样做的好处是:
- 解耦输入与渲染:
handle_input只修改资源,apply_preset只读资源并操作组件 - 便于序列化:资源可以轻松保存到文件,实现"设置存档"
- 便于 UI 集成:后续可以接入
bevy_ui的滑块和开关,只需绑定到资源字段
这种"设置资源 → 反映到渲染组件"的模式在 Bevy 项目中非常普遍。