Skip to main content

auto_exposure/
auto_exposure.rs

1//! This example showcases auto exposure,
2//! which automatically (but not instantly) adjusts the brightness of the scene in a way that mimics the function of the human eye.
3//! Auto exposure requires compute shader capabilities, so it's not available on WebGL.
4//!
5//! ## Controls
6//!
7//! | Key Binding        | Action                                 |
8//! |:-------------------|:---------------------------------------|
9//! | `Left` / `Right`   | Rotate Camera                          |
10//! | `C`                | Toggle Compensation Curve              |
11//! | `M`                | Toggle Metering Mask                   |
12//! | `V`                | Visualize Metering Mask                |
13
14use bevy::{
15    curve::cubic_splines::LinearSpline,
16    light::Skybox,
17    math::vec2,
18    post_process::auto_exposure::{
19        AutoExposure, AutoExposureCompensationCurve, AutoExposurePlugin,
20    },
21    prelude::*,
22    shape::Plane3d,
23};
24
25fn main() {
26    App::new()
27        .add_plugins(DefaultPlugins)
28        .add_plugins(AutoExposurePlugin)
29        .add_systems(Startup, setup)
30        .add_systems(Update, example_control_system)
31        .run();
32}
33
34fn setup(
35    mut commands: Commands,
36    mut meshes: ResMut<Assets<Mesh>>,
37    mut materials: ResMut<Assets<StandardMaterial>>,
38    mut compensation_curves: ResMut<Assets<AutoExposureCompensationCurve>>,
39    asset_server: Res<AssetServer>,
40) {
41    let metering_mask = asset_server.load("textures/basic_metering_mask.png");
42
43    commands.spawn((
44        Camera3d::default(),
45        Transform::from_xyz(1.0, 0.0, 0.0).looking_at(Vec3::ZERO, Vec3::Y),
46        AutoExposure {
47            metering_mask: metering_mask.clone(),
48            ..default()
49        },
50        Skybox {
51            image: Some(asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2")),
52            brightness: light_consts::lux::DIRECT_SUNLIGHT,
53            ..default()
54        },
55    ));
56
57    commands.insert_resource(ExampleResources {
58        basic_compensation_curve: compensation_curves.add(
59            AutoExposureCompensationCurve::from_curve(LinearSpline::new([
60                vec2(-4.0, -2.0),
61                vec2(0.0, 0.0),
62                vec2(2.0, 0.0),
63                vec2(4.0, 2.0),
64            ]))
65            .unwrap(),
66        ),
67        basic_metering_mask: metering_mask.clone(),
68    });
69
70    let plane = meshes.add(Mesh::from(
71        Plane3d {
72            normal: -Dir3::Z,
73            half_size: Vec2::new(2.0, 0.5),
74        }
75        .mesh(),
76    ));
77
78    // Build a dimly lit box around the camera, with a slot to see the bright skybox.
79    for level in -1..=1 {
80        for side in [-Vec3::X, Vec3::X, -Vec3::Z, Vec3::Z] {
81            if level == 0 && Vec3::Z == side {
82                continue;
83            }
84
85            let height = Vec3::Y * level as f32;
86
87            commands.spawn((
88                Mesh3d(plane.clone()),
89                MeshMaterial3d(materials.add(StandardMaterial {
90                    base_color: Color::srgb(
91                        0.5 + side.x * 0.5,
92                        0.75 - level as f32 * 0.25,
93                        0.5 + side.z * 0.5,
94                    ),
95                    ..default()
96                })),
97                Transform::from_translation(side * 2.0 + height).looking_at(height, Vec3::Y),
98            ));
99        }
100    }
101
102    commands.insert_resource(GlobalAmbientLight {
103        color: Color::WHITE,
104        brightness: 0.0,
105        ..default()
106    });
107
108    commands.spawn((
109        PointLight {
110            intensity: 2000.0,
111            ..default()
112        },
113        Transform::from_xyz(0.0, 0.0, 0.0),
114    ));
115
116    commands.spawn((
117        ImageNode {
118            image: metering_mask,
119            ..default()
120        },
121        Node {
122            width: percent(100),
123            height: percent(100),
124            ..default()
125        },
126    ));
127
128    let text_font = TextFont::default();
129
130    commands.spawn((Text::new("Left / Right - Rotate Camera\nC - Toggle Compensation Curve\nM - Toggle Metering Mask\nV - Visualize Metering Mask"),
131            text_font.clone(), Node {
132            position_type: PositionType::Absolute,
133            top: px(12),
134            left: px(12),
135            ..default()
136        })
137    );
138
139    commands.spawn((
140        Text::default(),
141        text_font,
142        Node {
143            position_type: PositionType::Absolute,
144            top: px(12),
145            right: px(12),
146            ..default()
147        },
148        ExampleDisplay,
149    ));
150}
151
152#[derive(Component)]
153struct ExampleDisplay;
154
155#[derive(Resource)]
156struct ExampleResources {
157    basic_compensation_curve: Handle<AutoExposureCompensationCurve>,
158    basic_metering_mask: Handle<Image>,
159}
160
161fn example_control_system(
162    camera: Single<(&mut Transform, &mut AutoExposure), With<Camera3d>>,
163    mut display: Single<&mut Text, With<ExampleDisplay>>,
164    mut mask_image: Single<&mut Node, With<ImageNode>>,
165    time: Res<Time>,
166    input: Res<ButtonInput<KeyCode>>,
167    resources: Res<ExampleResources>,
168) {
169    let (mut camera_transform, mut auto_exposure) = camera.into_inner();
170
171    let rotation = if input.pressed(KeyCode::ArrowLeft) {
172        time.delta_secs()
173    } else if input.pressed(KeyCode::ArrowRight) {
174        -time.delta_secs()
175    } else {
176        0.0
177    };
178
179    camera_transform.rotate_around(Vec3::ZERO, Quat::from_rotation_y(rotation));
180
181    if input.just_pressed(KeyCode::KeyC) {
182        auto_exposure.compensation_curve =
183            if auto_exposure.compensation_curve == resources.basic_compensation_curve {
184                Handle::default()
185            } else {
186                resources.basic_compensation_curve.clone()
187            };
188    }
189
190    if input.just_pressed(KeyCode::KeyM) {
191        auto_exposure.metering_mask =
192            if auto_exposure.metering_mask == resources.basic_metering_mask {
193                Handle::default()
194            } else {
195                resources.basic_metering_mask.clone()
196            };
197    }
198
199    mask_image.display = if input.pressed(KeyCode::KeyV) {
200        Display::Flex
201    } else {
202        Display::None
203    };
204
205    display.0 = format!(
206        "Compensation Curve: {}\nMetering Mask: {}",
207        if auto_exposure.compensation_curve == resources.basic_compensation_curve {
208            "Enabled"
209        } else {
210            "Disabled"
211        },
212        if auto_exposure.metering_mask == resources.basic_metering_mask {
213            "Enabled"
214        } else {
215            "Disabled"
216        },
217    );
218}