Skip to content

画质开关面板

把前几节学到的后处理效果组装成一个统一的画质控制面板。这个示例展示了如何在运行时动态添加、移除组件来切换效果——这也是 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 资源中,而不是直接操作组件。这样做的好处是:

  1. 解耦输入与渲染handle_input 只修改资源,apply_preset 只读资源并操作组件
  2. 便于序列化:资源可以轻松保存到文件,实现"设置存档"
  3. 便于 UI 集成:后续可以接入 bevy_ui 的滑块和开关,只需绑定到资源字段

这种"设置资源 → 反映到渲染组件"的模式在 Bevy 项目中非常普遍。