Skip to main content

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