color_grading/
color_grading.rs

1//! Demonstrates color grading with an interactive adjustment UI.
2
3use std::{
4    f32::consts::PI,
5    fmt::{self, Formatter},
6};
7
8use bevy::{
9    light::CascadeShadowConfigBuilder,
10    prelude::*,
11    render::view::{ColorGrading, ColorGradingGlobal, ColorGradingSection, Hdr},
12};
13use std::fmt::Display;
14
15static FONT_PATH: &str = "fonts/FiraMono-Medium.ttf";
16
17/// How quickly the value changes per frame.
18const OPTION_ADJUSTMENT_SPEED: f32 = 0.003;
19
20/// The color grading section that the user has selected: highlights, midtones,
21/// or shadows.
22#[derive(Clone, Copy, PartialEq)]
23enum SelectedColorGradingSection {
24    Highlights,
25    Midtones,
26    Shadows,
27}
28
29/// The global option that the user has selected.
30///
31/// See the documentation of [`ColorGradingGlobal`] for more information about
32/// each field here.
33#[derive(Clone, Copy, PartialEq, Default)]
34enum SelectedGlobalColorGradingOption {
35    #[default]
36    Exposure,
37    Temperature,
38    Tint,
39    Hue,
40}
41
42/// The section-specific option that the user has selected.
43///
44/// See the documentation of [`ColorGradingSection`] for more information about
45/// each field here.
46#[derive(Clone, Copy, PartialEq)]
47enum SelectedSectionColorGradingOption {
48    Saturation,
49    Contrast,
50    Gamma,
51    Gain,
52    Lift,
53}
54
55/// The color grading option that the user has selected.
56#[derive(Clone, Copy, PartialEq, Resource)]
57enum SelectedColorGradingOption {
58    /// The user has selected a global color grading option: one that applies to
59    /// the whole image as opposed to specifically to highlights, midtones, or
60    /// shadows.
61    Global(SelectedGlobalColorGradingOption),
62
63    /// The user has selected a color grading option that applies only to
64    /// highlights, midtones, or shadows.
65    Section(
66        SelectedColorGradingSection,
67        SelectedSectionColorGradingOption,
68    ),
69}
70
71impl Default for SelectedColorGradingOption {
72    fn default() -> Self {
73        Self::Global(default())
74    }
75}
76
77/// Buttons consist of three parts: the button itself, a label child, and a
78/// value child. This specifies one of the three entities.
79#[derive(Clone, Copy, PartialEq, Component)]
80enum ColorGradingOptionWidgetType {
81    /// The parent button.
82    Button,
83    /// The label of the button.
84    Label,
85    /// The numerical value that the button displays.
86    Value,
87}
88
89#[derive(Clone, Copy, Component)]
90struct ColorGradingOptionWidget {
91    widget_type: ColorGradingOptionWidgetType,
92    option: SelectedColorGradingOption,
93}
94
95/// A marker component for the help text at the top left of the screen.
96#[derive(Clone, Copy, Component)]
97struct HelpText;
98
99fn main() {
100    App::new()
101        .add_plugins(DefaultPlugins)
102        .init_resource::<SelectedColorGradingOption>()
103        .add_systems(Startup, setup)
104        .add_systems(
105            Update,
106            (
107                handle_button_presses,
108                adjust_color_grading_option,
109                update_ui_state,
110            )
111                .chain(),
112        )
113        .run();
114}
115
116fn setup(
117    mut commands: Commands,
118    currently_selected_option: Res<SelectedColorGradingOption>,
119    asset_server: Res<AssetServer>,
120) {
121    // Create the scene.
122    add_basic_scene(&mut commands, &asset_server);
123
124    // Create the root UI element.
125    let font = asset_server.load(FONT_PATH);
126    let color_grading = ColorGrading::default();
127    add_buttons(&mut commands, &font, &color_grading);
128
129    // Spawn help text.
130    add_help_text(&mut commands, &font, &currently_selected_option);
131
132    // Spawn the camera.
133    add_camera(&mut commands, &asset_server, color_grading);
134}
135
136/// Adds all the buttons on the bottom of the scene.
137fn add_buttons(commands: &mut Commands, font: &Handle<Font>, color_grading: &ColorGrading) {
138    commands.spawn((
139        // Spawn the parent node that contains all the buttons.
140        Node {
141            flex_direction: FlexDirection::Column,
142            position_type: PositionType::Absolute,
143            row_gap: px(6),
144            left: px(12),
145            bottom: px(12),
146            ..default()
147        },
148        children![
149            // Create the first row, which contains the global controls.
150            buttons_for_global_controls(color_grading, font),
151            // Create the rows for individual controls.
152            buttons_for_section(SelectedColorGradingSection::Highlights, color_grading, font),
153            buttons_for_section(SelectedColorGradingSection::Midtones, color_grading, font),
154            buttons_for_section(SelectedColorGradingSection::Shadows, color_grading, font),
155        ],
156    ));
157}
158
159/// Adds the buttons for the global controls (those that control the scene as a
160/// whole as opposed to shadows, midtones, or highlights).
161fn buttons_for_global_controls(color_grading: &ColorGrading, font: &Handle<Font>) -> impl Bundle {
162    let make_button = |option: SelectedGlobalColorGradingOption| {
163        button_for_value(
164            SelectedColorGradingOption::Global(option),
165            color_grading,
166            font,
167        )
168    };
169
170    // Add the parent node for the row.
171    (
172        Node::default(),
173        children![
174            Node {
175                width: px(125),
176                ..default()
177            },
178            make_button(SelectedGlobalColorGradingOption::Exposure),
179            make_button(SelectedGlobalColorGradingOption::Temperature),
180            make_button(SelectedGlobalColorGradingOption::Tint),
181            make_button(SelectedGlobalColorGradingOption::Hue),
182        ],
183    )
184}
185
186/// Adds the buttons that control color grading for individual sections
187/// (highlights, midtones, shadows).
188fn buttons_for_section(
189    section: SelectedColorGradingSection,
190    color_grading: &ColorGrading,
191    font: &Handle<Font>,
192) -> impl Bundle {
193    let make_button = |option| {
194        button_for_value(
195            SelectedColorGradingOption::Section(section, option),
196            color_grading,
197            font,
198        )
199    };
200
201    // Spawn the row container.
202    (
203        Node {
204            align_items: AlignItems::Center,
205            ..default()
206        },
207        children![
208            // Spawn the label ("Highlights", etc.)
209            (
210                text(&section.to_string(), font, Color::WHITE),
211                Node {
212                    width: px(125),
213                    ..default()
214                }
215            ),
216            // Spawn the buttons.
217            make_button(SelectedSectionColorGradingOption::Saturation),
218            make_button(SelectedSectionColorGradingOption::Contrast),
219            make_button(SelectedSectionColorGradingOption::Gamma),
220            make_button(SelectedSectionColorGradingOption::Gain),
221            make_button(SelectedSectionColorGradingOption::Lift),
222        ],
223    )
224}
225
226/// Adds a button that controls one of the color grading values.
227fn button_for_value(
228    option: SelectedColorGradingOption,
229    color_grading: &ColorGrading,
230    font: &Handle<Font>,
231) -> impl Bundle {
232    let label = match option {
233        SelectedColorGradingOption::Global(option) => option.to_string(),
234        SelectedColorGradingOption::Section(_, option) => option.to_string(),
235    };
236
237    // Add the button node.
238    (
239        Button,
240        Node {
241            border: UiRect::all(px(1)),
242            width: px(200),
243            justify_content: JustifyContent::Center,
244            align_items: AlignItems::Center,
245            padding: UiRect::axes(px(12), px(6)),
246            margin: UiRect::right(px(12)),
247            ..default()
248        },
249        BorderColor::all(Color::WHITE),
250        BorderRadius::MAX,
251        BackgroundColor(Color::BLACK),
252        ColorGradingOptionWidget {
253            widget_type: ColorGradingOptionWidgetType::Button,
254            option,
255        },
256        children![
257            // Add the button label.
258            (
259                text(&label, font, Color::WHITE),
260                ColorGradingOptionWidget {
261                    widget_type: ColorGradingOptionWidgetType::Label,
262                    option,
263                },
264            ),
265            // Add a spacer.
266            Node {
267                flex_grow: 1.0,
268                ..default()
269            },
270            // Add the value text.
271            (
272                text(
273                    &format!("{:.3}", option.get(color_grading)),
274                    font,
275                    Color::WHITE,
276                ),
277                ColorGradingOptionWidget {
278                    widget_type: ColorGradingOptionWidgetType::Value,
279                    option,
280                },
281            ),
282        ],
283    )
284}
285
286/// Creates the help text at the top of the screen.
287fn add_help_text(
288    commands: &mut Commands,
289    font: &Handle<Font>,
290    currently_selected_option: &SelectedColorGradingOption,
291) {
292    commands.spawn((
293        Text::new(create_help_text(currently_selected_option)),
294        TextFont {
295            font: font.clone(),
296            ..default()
297        },
298        Node {
299            position_type: PositionType::Absolute,
300            left: px(12),
301            top: px(12),
302            ..default()
303        },
304        HelpText,
305    ));
306}
307
308/// Adds some text to the scene.
309fn text(label: &str, font: &Handle<Font>, color: Color) -> impl Bundle + use<> {
310    (
311        Text::new(label),
312        TextFont {
313            font: font.clone(),
314            font_size: 15.0,
315            ..default()
316        },
317        TextColor(color),
318    )
319}
320
321fn add_camera(commands: &mut Commands, asset_server: &AssetServer, color_grading: ColorGrading) {
322    commands.spawn((
323        Camera3d::default(),
324        Hdr,
325        Transform::from_xyz(0.7, 0.7, 1.0).looking_at(Vec3::new(0.0, 0.3, 0.0), Vec3::Y),
326        color_grading,
327        DistanceFog {
328            color: Color::srgb_u8(43, 44, 47),
329            falloff: FogFalloff::Linear {
330                start: 1.0,
331                end: 8.0,
332            },
333            ..default()
334        },
335        EnvironmentMapLight {
336            diffuse_map: asset_server.load("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2"),
337            specular_map: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"),
338            intensity: 2000.0,
339            ..default()
340        },
341    ));
342}
343
344fn add_basic_scene(commands: &mut Commands, asset_server: &AssetServer) {
345    // Spawn the main scene.
346    commands.spawn(SceneRoot(asset_server.load(
347        GltfAssetLabel::Scene(0).from_asset("models/TonemappingTest/TonemappingTest.gltf"),
348    )));
349
350    // Spawn the flight helmet.
351    commands.spawn((
352        SceneRoot(
353            asset_server
354                .load(GltfAssetLabel::Scene(0).from_asset("models/FlightHelmet/FlightHelmet.gltf")),
355        ),
356        Transform::from_xyz(0.5, 0.0, -0.5).with_rotation(Quat::from_rotation_y(-0.15 * PI)),
357    ));
358
359    // Spawn the light.
360    commands.spawn((
361        DirectionalLight {
362            illuminance: 15000.0,
363            shadows_enabled: true,
364            ..default()
365        },
366        Transform::from_rotation(Quat::from_euler(EulerRot::ZYX, 0.0, PI * -0.15, PI * -0.15)),
367        CascadeShadowConfigBuilder {
368            maximum_distance: 3.0,
369            first_cascade_far_bound: 0.9,
370            ..default()
371        }
372        .build(),
373    ));
374}
375
376impl Display for SelectedGlobalColorGradingOption {
377    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
378        let name = match *self {
379            SelectedGlobalColorGradingOption::Exposure => "Exposure",
380            SelectedGlobalColorGradingOption::Temperature => "Temperature",
381            SelectedGlobalColorGradingOption::Tint => "Tint",
382            SelectedGlobalColorGradingOption::Hue => "Hue",
383        };
384        f.write_str(name)
385    }
386}
387
388impl Display for SelectedColorGradingSection {
389    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
390        let name = match *self {
391            SelectedColorGradingSection::Highlights => "Highlights",
392            SelectedColorGradingSection::Midtones => "Midtones",
393            SelectedColorGradingSection::Shadows => "Shadows",
394        };
395        f.write_str(name)
396    }
397}
398
399impl Display for SelectedSectionColorGradingOption {
400    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
401        let name = match *self {
402            SelectedSectionColorGradingOption::Saturation => "Saturation",
403            SelectedSectionColorGradingOption::Contrast => "Contrast",
404            SelectedSectionColorGradingOption::Gamma => "Gamma",
405            SelectedSectionColorGradingOption::Gain => "Gain",
406            SelectedSectionColorGradingOption::Lift => "Lift",
407        };
408        f.write_str(name)
409    }
410}
411
412impl Display for SelectedColorGradingOption {
413    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
414        match self {
415            SelectedColorGradingOption::Global(option) => write!(f, "\"{option}\""),
416            SelectedColorGradingOption::Section(section, option) => {
417                write!(f, "\"{option}\" for \"{section}\"")
418            }
419        }
420    }
421}
422
423impl SelectedSectionColorGradingOption {
424    /// Returns the appropriate value in the given color grading section.
425    fn get(&self, section: &ColorGradingSection) -> f32 {
426        match *self {
427            SelectedSectionColorGradingOption::Saturation => section.saturation,
428            SelectedSectionColorGradingOption::Contrast => section.contrast,
429            SelectedSectionColorGradingOption::Gamma => section.gamma,
430            SelectedSectionColorGradingOption::Gain => section.gain,
431            SelectedSectionColorGradingOption::Lift => section.lift,
432        }
433    }
434
435    fn set(&self, section: &mut ColorGradingSection, value: f32) {
436        match *self {
437            SelectedSectionColorGradingOption::Saturation => section.saturation = value,
438            SelectedSectionColorGradingOption::Contrast => section.contrast = value,
439            SelectedSectionColorGradingOption::Gamma => section.gamma = value,
440            SelectedSectionColorGradingOption::Gain => section.gain = value,
441            SelectedSectionColorGradingOption::Lift => section.lift = value,
442        }
443    }
444}
445
446impl SelectedGlobalColorGradingOption {
447    /// Returns the appropriate value in the given set of global color grading
448    /// values.
449    fn get(&self, global: &ColorGradingGlobal) -> f32 {
450        match *self {
451            SelectedGlobalColorGradingOption::Exposure => global.exposure,
452            SelectedGlobalColorGradingOption::Temperature => global.temperature,
453            SelectedGlobalColorGradingOption::Tint => global.tint,
454            SelectedGlobalColorGradingOption::Hue => global.hue,
455        }
456    }
457
458    /// Sets the appropriate value in the given set of global color grading
459    /// values.
460    fn set(&self, global: &mut ColorGradingGlobal, value: f32) {
461        match *self {
462            SelectedGlobalColorGradingOption::Exposure => global.exposure = value,
463            SelectedGlobalColorGradingOption::Temperature => global.temperature = value,
464            SelectedGlobalColorGradingOption::Tint => global.tint = value,
465            SelectedGlobalColorGradingOption::Hue => global.hue = value,
466        }
467    }
468}
469
470impl SelectedColorGradingOption {
471    /// Returns the appropriate value in the given set of color grading values.
472    fn get(&self, color_grading: &ColorGrading) -> f32 {
473        match self {
474            SelectedColorGradingOption::Global(option) => option.get(&color_grading.global),
475            SelectedColorGradingOption::Section(
476                SelectedColorGradingSection::Highlights,
477                option,
478            ) => option.get(&color_grading.highlights),
479            SelectedColorGradingOption::Section(SelectedColorGradingSection::Midtones, option) => {
480                option.get(&color_grading.midtones)
481            }
482            SelectedColorGradingOption::Section(SelectedColorGradingSection::Shadows, option) => {
483                option.get(&color_grading.shadows)
484            }
485        }
486    }
487
488    /// Sets the appropriate value in the given set of color grading values.
489    fn set(&self, color_grading: &mut ColorGrading, value: f32) {
490        match self {
491            SelectedColorGradingOption::Global(option) => {
492                option.set(&mut color_grading.global, value);
493            }
494            SelectedColorGradingOption::Section(
495                SelectedColorGradingSection::Highlights,
496                option,
497            ) => option.set(&mut color_grading.highlights, value),
498            SelectedColorGradingOption::Section(SelectedColorGradingSection::Midtones, option) => {
499                option.set(&mut color_grading.midtones, value);
500            }
501            SelectedColorGradingOption::Section(SelectedColorGradingSection::Shadows, option) => {
502                option.set(&mut color_grading.shadows, value);
503            }
504        }
505    }
506}
507
508/// Handles mouse clicks on the buttons when the user clicks on a new one.
509fn handle_button_presses(
510    mut interactions: Query<(&Interaction, &ColorGradingOptionWidget), Changed<Interaction>>,
511    mut currently_selected_option: ResMut<SelectedColorGradingOption>,
512) {
513    for (interaction, widget) in interactions.iter_mut() {
514        if widget.widget_type == ColorGradingOptionWidgetType::Button
515            && *interaction == Interaction::Pressed
516        {
517            *currently_selected_option = widget.option;
518        }
519    }
520}
521
522/// Updates the state of the UI based on the current state.
523fn update_ui_state(
524    mut buttons: Query<(
525        &mut BackgroundColor,
526        &mut BorderColor,
527        &ColorGradingOptionWidget,
528    )>,
529    button_text: Query<(Entity, &ColorGradingOptionWidget), (With<Text>, Without<HelpText>)>,
530    help_text: Single<Entity, With<HelpText>>,
531    mut writer: TextUiWriter,
532    cameras: Single<Ref<ColorGrading>>,
533    currently_selected_option: Res<SelectedColorGradingOption>,
534) {
535    // Exit early if the UI didn't change
536    if !currently_selected_option.is_changed() && !cameras.is_changed() {
537        return;
538    }
539
540    // The currently-selected option is drawn with inverted colors.
541    for (mut background, mut border_color, widget) in buttons.iter_mut() {
542        if *currently_selected_option == widget.option {
543            *background = Color::WHITE.into();
544            *border_color = Color::BLACK.into();
545        } else {
546            *background = Color::BLACK.into();
547            *border_color = Color::WHITE.into();
548        }
549    }
550
551    let value_label = format!("{:.3}", currently_selected_option.get(cameras.as_ref()));
552
553    // Update the buttons.
554    for (entity, widget) in button_text.iter() {
555        // Set the text color.
556
557        let color = if *currently_selected_option == widget.option {
558            Color::BLACK
559        } else {
560            Color::WHITE
561        };
562
563        writer.for_each_color(entity, |mut text_color| {
564            text_color.0 = color;
565        });
566
567        // Update the displayed value, if this is the currently-selected option.
568        if widget.widget_type == ColorGradingOptionWidgetType::Value
569            && *currently_selected_option == widget.option
570        {
571            writer.for_each_text(entity, |mut text| {
572                text.clone_from(&value_label);
573            });
574        }
575    }
576
577    // Update the help text.
578    *writer.text(*help_text, 0) = create_help_text(&currently_selected_option);
579}
580
581/// Creates the help text at the top left of the window.
582fn create_help_text(currently_selected_option: &SelectedColorGradingOption) -> String {
583    format!("Press Left/Right to adjust {currently_selected_option}")
584}
585
586/// Processes keyboard input to change the value of the currently-selected color
587/// grading option.
588fn adjust_color_grading_option(
589    mut color_grading: Single<&mut ColorGrading>,
590    input: Res<ButtonInput<KeyCode>>,
591    currently_selected_option: Res<SelectedColorGradingOption>,
592) {
593    let mut delta = 0.0;
594    if input.pressed(KeyCode::ArrowLeft) {
595        delta -= OPTION_ADJUSTMENT_SPEED;
596    }
597    if input.pressed(KeyCode::ArrowRight) {
598        delta += OPTION_ADJUSTMENT_SPEED;
599    }
600
601    if delta != 0.0 {
602        let new_value = currently_selected_option.get(color_grading.as_ref()) + delta;
603        currently_selected_option.set(&mut color_grading, new_value);
604    }
605}