post_processing/
post_processing.rs

1//! Demonstrates Bevy's built-in postprocessing features.
2//!
3//! Currently, this simply consists of chromatic aberration.
4
5use std::f32::consts::PI;
6
7use bevy::{
8    light::CascadeShadowConfigBuilder, post_process::effect_stack::ChromaticAberration, prelude::*,
9    render::view::Hdr,
10};
11
12/// The number of units per frame to add to or subtract from intensity when the
13/// arrow keys are held.
14const CHROMATIC_ABERRATION_INTENSITY_ADJUSTMENT_SPEED: f32 = 0.002;
15
16/// The maximum supported chromatic aberration intensity level.
17const MAX_CHROMATIC_ABERRATION_INTENSITY: f32 = 0.4;
18
19/// The settings that the user can control.
20#[derive(Resource)]
21struct AppSettings {
22    /// The intensity of the chromatic aberration effect.
23    chromatic_aberration_intensity: f32,
24}
25
26/// The entry point.
27fn main() {
28    App::new()
29        .init_resource::<AppSettings>()
30        .add_plugins(DefaultPlugins.set(WindowPlugin {
31            primary_window: Some(Window {
32                title: "Bevy Chromatic Aberration Example".into(),
33                ..default()
34            }),
35            ..default()
36        }))
37        .add_systems(Startup, setup)
38        .add_systems(Update, handle_keyboard_input)
39        .add_systems(
40            Update,
41            (update_chromatic_aberration_settings, update_help_text)
42                .run_if(resource_changed::<AppSettings>)
43                .after(handle_keyboard_input),
44        )
45        .run();
46}
47
48/// Creates the example scene and spawns the UI.
49fn setup(mut commands: Commands, asset_server: Res<AssetServer>, app_settings: Res<AppSettings>) {
50    // Spawn the camera.
51    spawn_camera(&mut commands, &asset_server);
52
53    // Create the scene.
54    spawn_scene(&mut commands, &asset_server);
55
56    // Spawn the help text.
57    spawn_text(&mut commands, &app_settings);
58}
59
60/// Spawns the camera, including the [`ChromaticAberration`] component.
61fn spawn_camera(commands: &mut Commands, asset_server: &AssetServer) {
62    commands.spawn((
63        Camera3d::default(),
64        Hdr,
65        Transform::from_xyz(0.7, 0.7, 1.0).looking_at(Vec3::new(0.0, 0.3, 0.0), Vec3::Y),
66        DistanceFog {
67            color: Color::srgb_u8(43, 44, 47),
68            falloff: FogFalloff::Linear {
69                start: 1.0,
70                end: 8.0,
71            },
72            ..default()
73        },
74        EnvironmentMapLight {
75            diffuse_map: asset_server.load("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2"),
76            specular_map: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"),
77            intensity: 2000.0,
78            ..default()
79        },
80        // Include the `ChromaticAberration` component.
81        ChromaticAberration::default(),
82    ));
83}
84
85/// Spawns the scene.
86///
87/// This is just the tonemapping test scene, chosen for the fact that it uses a
88/// variety of colors.
89fn spawn_scene(commands: &mut Commands, asset_server: &AssetServer) {
90    // Spawn the main scene.
91    commands.spawn(SceneRoot(asset_server.load(
92        GltfAssetLabel::Scene(0).from_asset("models/TonemappingTest/TonemappingTest.gltf"),
93    )));
94
95    // Spawn the flight helmet.
96    commands.spawn((
97        SceneRoot(
98            asset_server
99                .load(GltfAssetLabel::Scene(0).from_asset("models/FlightHelmet/FlightHelmet.gltf")),
100        ),
101        Transform::from_xyz(0.5, 0.0, -0.5).with_rotation(Quat::from_rotation_y(-0.15 * PI)),
102    ));
103
104    // Spawn the light.
105    commands.spawn((
106        DirectionalLight {
107            illuminance: 15000.0,
108            shadows_enabled: true,
109            ..default()
110        },
111        Transform::from_rotation(Quat::from_euler(EulerRot::ZYX, 0.0, PI * -0.15, PI * -0.15)),
112        CascadeShadowConfigBuilder {
113            maximum_distance: 3.0,
114            first_cascade_far_bound: 0.9,
115            ..default()
116        }
117        .build(),
118    ));
119}
120
121/// Spawns the help text at the bottom of the screen.
122fn spawn_text(commands: &mut Commands, app_settings: &AppSettings) {
123    commands.spawn((
124        create_help_text(app_settings),
125        Node {
126            position_type: PositionType::Absolute,
127            bottom: px(12),
128            left: px(12),
129            ..default()
130        },
131    ));
132}
133
134impl Default for AppSettings {
135    fn default() -> Self {
136        Self {
137            chromatic_aberration_intensity: ChromaticAberration::default().intensity,
138        }
139    }
140}
141
142/// Creates help text at the bottom of the screen.
143fn create_help_text(app_settings: &AppSettings) -> Text {
144    format!(
145        "Chromatic aberration intensity: {:.2} (Press Left or Right to change)",
146        app_settings.chromatic_aberration_intensity
147    )
148    .into()
149}
150
151/// Handles requests from the user to change the chromatic aberration intensity.
152fn handle_keyboard_input(mut app_settings: ResMut<AppSettings>, input: Res<ButtonInput<KeyCode>>) {
153    let mut delta = 0.0;
154    if input.pressed(KeyCode::ArrowLeft) {
155        delta -= CHROMATIC_ABERRATION_INTENSITY_ADJUSTMENT_SPEED;
156    } else if input.pressed(KeyCode::ArrowRight) {
157        delta += CHROMATIC_ABERRATION_INTENSITY_ADJUSTMENT_SPEED;
158    }
159
160    // If no arrow key was pressed, just bail out.
161    if delta == 0.0 {
162        return;
163    }
164
165    app_settings.chromatic_aberration_intensity = (app_settings.chromatic_aberration_intensity
166        + delta)
167        .clamp(0.0, MAX_CHROMATIC_ABERRATION_INTENSITY);
168}
169
170/// Updates the [`ChromaticAberration`] settings per the [`AppSettings`].
171fn update_chromatic_aberration_settings(
172    mut chromatic_aberration: Query<&mut ChromaticAberration>,
173    app_settings: Res<AppSettings>,
174) {
175    let intensity = app_settings.chromatic_aberration_intensity;
176
177    // Pick a reasonable maximum sample size for the intensity to avoid an
178    // artifact whereby the individual samples appear instead of producing
179    // smooth streaks of color.
180    //
181    // Don't take this formula too seriously; it hasn't been heavily tuned.
182    let max_samples = ((intensity - 0.02) / (0.20 - 0.02) * 56.0 + 8.0)
183        .clamp(8.0, 64.0)
184        .round() as u32;
185
186    for mut chromatic_aberration in &mut chromatic_aberration {
187        chromatic_aberration.intensity = intensity;
188        chromatic_aberration.max_samples = max_samples;
189    }
190}
191
192/// Updates the help text at the bottom of the screen to reflect the current
193/// [`AppSettings`].
194fn update_help_text(mut text: Query<&mut Text>, app_settings: Res<AppSettings>) {
195    for mut text in text.iter_mut() {
196        *text = create_help_text(&app_settings);
197    }
198}