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