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    feathers::{
11        containers::{pane, pane_body, pane_header},
12        controls::{FeathersNumberInput, HardLimit, NumberInputPrecision, NumberInputValue},
13        dark_theme::create_dark_theme,
14        display::label,
15        theme::{ThemeProps, UiTheme},
16        tokens::semantic::{SURFACE_PANE_BODY, SURFACE_PANE_HEADER},
17        FeathersPlugins,
18    },
19    light::CascadeShadowConfigBuilder,
20    prelude::*,
21    render::view::{ColorGrading, ColorGradingGlobal, ColorGradingSection},
22    ui_widgets::ValueChange,
23};
24use std::fmt::Display;
25
26/// The global color grading settings that the user can modify.
27///
28/// See the documentation of [`ColorGradingGlobal`] for more information about
29/// each field here.
30#[derive(Clone, Copy, PartialEq, Default)]
31enum GlobalColorGradingSetting {
32    #[default]
33    Exposure,
34    Temperature,
35    Tint,
36    Hue,
37}
38
39/// A color grading section that the user can modify the settings of:
40/// highlights, midtones, or shadows.
41#[derive(Clone, Copy, PartialEq, Default)]
42enum SectionColorGradingName {
43    #[default]
44    Highlights,
45    Midtones,
46    Shadows,
47}
48
49/// The section-specific color grading setting that the user can modify.
50///
51/// See the documentation of [`ColorGradingSection`] for more information about
52/// each field here.
53#[derive(Clone, Copy, PartialEq, Default)]
54enum SectionColorGradingSetting {
55    #[default]
56    Saturation,
57    Contrast,
58    Gamma,
59    Gain,
60    Lift,
61}
62
63/// A color grading settings that the user can modify.
64#[derive(Component, Clone, Copy, PartialEq)]
65enum ColorGradingSetting {
66    /// The global color grading settings. They apply to
67    /// the whole image as opposed to specifically to highlights, midtones, or
68    /// shadows.
69    Global(GlobalColorGradingSetting),
70
71    /// A color grading setting that applies only to highlights, midtones, or shadows.
72    Section(SectionColorGradingName, SectionColorGradingSetting),
73}
74
75impl Default for ColorGradingSetting {
76    fn default() -> Self {
77        Self::Global(default())
78    }
79}
80
81fn main() {
82    App::new()
83        .add_plugins((DefaultPlugins, FeathersPlugins))
84        .insert_resource(UiTheme(get_example_theme()))
85        .add_systems(Startup, setup)
86        .add_observer(handle_value_change_number_input)
87        .run();
88}
89
90fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
91    // Create the scene.
92    add_basic_scene(&mut commands, &asset_server);
93
94    // Create the root UI element.
95    let color_grading = ColorGrading::default();
96    add_buttons(&mut commands, &color_grading);
97
98    add_help_text(&mut commands);
99
100    // Spawn the camera.
101    add_camera(&mut commands, &asset_server, color_grading);
102}
103
104/// Adds all the buttons on the bottom of the scene.
105fn add_buttons(commands: &mut Commands, color_grading: &ColorGrading) {
106    commands.spawn_scene(bsn! {
107        // Spawn the parent node that contains all the buttons.
108        Node {
109            flex_direction: FlexDirection::Row,
110            position_type: PositionType::Absolute,
111            column_gap: px(6),
112            left: px(12),
113            bottom: px(12),
114        }
115        Children [
116            // Create the first pane, which contains the global controls.
117            @pane_for_global_controls(color_grading)
118            --
119            // Create the following panes for individual controls.
120            @pane_for_section(SectionColorGradingName::Highlights, color_grading)
121            --
122            @pane_for_section(SectionColorGradingName::Midtones, color_grading)
123            --
124            @pane_for_section(SectionColorGradingName::Shadows, color_grading)
125        ]
126    });
127}
128
129/// Adds the buttons for the global controls (those that control the scene as a
130/// whole as opposed to shadows, midtones, or highlights).
131fn pane_for_global_controls(color_grading: &ColorGrading) -> impl Scene {
132    let make_button =
133        |option| number_input_for_value(ColorGradingSetting::Global(option), color_grading);
134
135    bsn! {
136        @pane()
137        Children [
138            // Spawn the label ("Highlights", etc.)
139            @pane_header()
140            Children[
141                Node {
142                    width: px(120)
143                    align_self: AlignSelf::Start,
144                }
145                Children [
146                    @label("Global Settings")
147                ]
148            ]
149            --
150            // Spawn the buttons
151            @pane_body()
152            Children [
153                @make_button(GlobalColorGradingSetting::Exposure)
154                --
155                @make_button(GlobalColorGradingSetting::Temperature)
156                --
157                @make_button(GlobalColorGradingSetting::Tint)
158                --
159                @make_button(GlobalColorGradingSetting::Hue)
160            ]
161        ]
162    }
163}
164
165/// Adds the buttons that control color grading for individual sections
166/// (highlights, midtones, shadows).
167fn pane_for_section(section: SectionColorGradingName, color_grading: &ColorGrading) -> impl Scene {
168    let make_button = |setting| {
169        number_input_for_value(
170            ColorGradingSetting::Section(section, setting),
171            color_grading,
172        )
173    };
174
175    bsn! {
176        @pane()
177        Children [
178            // Spawn the label ("Highlights", etc.)
179            @pane_header() Children [
180                Node {
181                    width: px(120),
182                    align_self: AlignSelf::Start,
183                }
184                Children [
185                    @label(section.to_string())
186                ]
187            ]
188            --
189            // Spawn the buttons.
190            @pane_body() Children[
191                @make_button(SectionColorGradingSetting::Saturation)
192                --
193                @make_button(SectionColorGradingSetting::Contrast)
194                --
195                @make_button(SectionColorGradingSetting::Gamma)
196                --
197                @make_button(SectionColorGradingSetting::Gain)
198                --
199                @make_button(SectionColorGradingSetting::Lift)
200            ]
201        ]
202    }
203}
204
205/// Adds a feathers number input that controls one of the color grading values.
206fn number_input_for_value(
207    setting: ColorGradingSetting,
208    color_grading: &ColorGrading,
209) -> impl Scene {
210    let setting_label = match setting {
211        ColorGradingSetting::Global(setting) => setting.to_string(),
212        ColorGradingSetting::Section(_, setting) => setting.to_string(),
213    };
214
215    bsn! {
216        Node {
217            justify_content: JustifyContent::Center,
218            align_items: AlignItems::Center,
219        }
220        Children [
221            Node {
222                width: px(120),
223            }
224            Children[
225                @label(setting_label)
226            ]
227            --
228            Node {
229                align_items: AlignItems::Center,
230                width: px(50),
231            }
232            @FeathersNumberInput
233            NumberInputValue::F32({setting.get(color_grading)})
234            setting
235            NumberInputPrecision(2)
236            HardLimit::f32(0. ..=10.)
237        ]
238    }
239}
240
241fn add_camera(commands: &mut Commands, asset_server: &AssetServer, color_grading: ColorGrading) {
242    commands.spawn((
243        Camera3d::default(),
244        Hdr,
245        Transform::from_xyz(0.7, 0.7, 1.0).looking_at(Vec3::new(0.0, 0.3, 0.0), Vec3::Y),
246        color_grading,
247        DistanceFog {
248            color: Color::srgb_u8(43, 44, 47),
249            falloff: FogFalloff::Linear {
250                start: 1.0,
251                end: 8.0,
252            },
253            ..default()
254        },
255        EnvironmentMapLight {
256            diffuse_map: asset_server.load("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2"),
257            specular_map: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"),
258            intensity: 2000.0,
259            ..default()
260        },
261    ));
262}
263
264fn add_basic_scene(commands: &mut Commands, asset_server: &AssetServer) {
265    // Spawn the main scene.
266    commands.spawn(WorldAssetRoot(asset_server.load(
267        GltfAssetLabel::Scene(0).from_asset("models/TonemappingTest/TonemappingTest.gltf"),
268    )));
269
270    // Spawn the flight helmet.
271    commands.spawn((
272        WorldAssetRoot(
273            asset_server
274                .load(GltfAssetLabel::Scene(0).from_asset("models/FlightHelmet/FlightHelmet.gltf")),
275        ),
276        Transform::from_xyz(0.5, 0.0, -0.5).with_rotation(Quat::from_rotation_y(-0.15 * PI)),
277    ));
278
279    // Spawn the light.
280    commands.spawn((
281        DirectionalLight {
282            illuminance: 15000.0,
283            shadow_maps_enabled: true,
284            ..default()
285        },
286        Transform::from_rotation(Quat::from_euler(EulerRot::ZYX, 0.0, PI * -0.15, PI * -0.15)),
287        CascadeShadowConfigBuilder {
288            maximum_distance: 3.0,
289            first_cascade_far_bound: 0.9,
290            ..default()
291        }
292        .build(),
293    ));
294}
295
296/// Observer that handles changes to number inputs.
297fn handle_value_change_number_input(
298    value_change: On<ValueChange<f32>>,
299    mut commands: Commands,
300    setting_q: Query<&ColorGradingSetting, With<FeathersNumberInput>>,
301    mut color_grading: Single<&mut ColorGrading>,
302) {
303    if let Ok(setting) = setting_q.get(value_change.source) {
304        setting.set(&mut color_grading, value_change.value);
305
306        commands
307            .entity(value_change.source)
308            .insert(NumberInputValue::F32(value_change.value));
309    }
310}
311
312impl Display for GlobalColorGradingSetting {
313    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
314        let name = match *self {
315            GlobalColorGradingSetting::Exposure => "Exposure",
316            GlobalColorGradingSetting::Temperature => "Temperature",
317            GlobalColorGradingSetting::Tint => "Tint",
318            GlobalColorGradingSetting::Hue => "Hue",
319        };
320        f.write_str(name)
321    }
322}
323
324impl Display for SectionColorGradingName {
325    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
326        let name = match *self {
327            SectionColorGradingName::Highlights => "Highlights",
328            SectionColorGradingName::Midtones => "Midtones",
329            SectionColorGradingName::Shadows => "Shadows",
330        };
331        f.write_str(name)
332    }
333}
334
335impl Display for SectionColorGradingSetting {
336    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
337        let name = match *self {
338            SectionColorGradingSetting::Saturation => "Saturation",
339            SectionColorGradingSetting::Contrast => "Contrast",
340            SectionColorGradingSetting::Gamma => "Gamma",
341            SectionColorGradingSetting::Gain => "Gain",
342            SectionColorGradingSetting::Lift => "Lift",
343        };
344        f.write_str(name)
345    }
346}
347
348impl Display for ColorGradingSetting {
349    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
350        match self {
351            ColorGradingSetting::Global(option) => write!(f, "\"{option}\""),
352            ColorGradingSetting::Section(section, option) => {
353                write!(f, "\"{option}\" for \"{section}\"")
354            }
355        }
356    }
357}
358
359impl SectionColorGradingSetting {
360    /// Returns the appropriate value in the given color grading section.
361    fn get(&self, section: &ColorGradingSection) -> f32 {
362        match *self {
363            SectionColorGradingSetting::Saturation => section.saturation,
364            SectionColorGradingSetting::Contrast => section.contrast,
365            SectionColorGradingSetting::Gamma => section.gamma,
366            SectionColorGradingSetting::Gain => section.gain,
367            SectionColorGradingSetting::Lift => section.lift,
368        }
369    }
370
371    /// Sets the appropriate value in the given set of color grading values.
372    fn set(&self, section: &mut ColorGradingSection, value: f32) {
373        match *self {
374            SectionColorGradingSetting::Saturation => section.saturation = value,
375            SectionColorGradingSetting::Contrast => section.contrast = value,
376            SectionColorGradingSetting::Gamma => section.gamma = value,
377            SectionColorGradingSetting::Gain => section.gain = value,
378            SectionColorGradingSetting::Lift => section.lift = value,
379        }
380    }
381}
382
383impl GlobalColorGradingSetting {
384    /// Returns the appropriate value in the given set of global color grading
385    /// values.
386    fn get(&self, global: &ColorGradingGlobal) -> f32 {
387        match *self {
388            GlobalColorGradingSetting::Exposure => global.exposure,
389            GlobalColorGradingSetting::Temperature => global.temperature,
390            GlobalColorGradingSetting::Tint => global.tint,
391            GlobalColorGradingSetting::Hue => global.hue,
392        }
393    }
394
395    /// Sets the appropriate value in the given set of global color grading
396    /// values.
397    fn set(&self, global: &mut ColorGradingGlobal, value: f32) {
398        match *self {
399            GlobalColorGradingSetting::Exposure => global.exposure = value,
400            GlobalColorGradingSetting::Temperature => global.temperature = value,
401            GlobalColorGradingSetting::Tint => global.tint = value,
402            GlobalColorGradingSetting::Hue => global.hue = value,
403        }
404    }
405}
406
407impl ColorGradingSetting {
408    /// Returns the appropriate value in the given set of color grading values.
409    fn get(&self, color_grading: &ColorGrading) -> f32 {
410        match self {
411            ColorGradingSetting::Global(option) => option.get(&color_grading.global),
412            ColorGradingSetting::Section(SectionColorGradingName::Highlights, option) => {
413                option.get(&color_grading.highlights)
414            }
415            ColorGradingSetting::Section(SectionColorGradingName::Midtones, option) => {
416                option.get(&color_grading.midtones)
417            }
418            ColorGradingSetting::Section(SectionColorGradingName::Shadows, option) => {
419                option.get(&color_grading.shadows)
420            }
421        }
422    }
423
424    /// Sets the appropriate value in the given set of color grading values.
425    fn set(&self, color_grading: &mut ColorGrading, value: f32) {
426        match self {
427            ColorGradingSetting::Global(option) => {
428                option.set(&mut color_grading.global, value);
429            }
430            ColorGradingSetting::Section(SectionColorGradingName::Highlights, option) => {
431                option.set(&mut color_grading.highlights, value);
432            }
433            ColorGradingSetting::Section(SectionColorGradingName::Midtones, option) => {
434                option.set(&mut color_grading.midtones, value);
435            }
436            ColorGradingSetting::Section(SectionColorGradingName::Shadows, option) => {
437                option.set(&mut color_grading.shadows, value);
438            }
439        }
440    }
441}
442
443/// Get the Feathers Theme for the example. It is a slightly modified dark theme.
444fn get_example_theme() -> ThemeProps {
445    let mut props = create_dark_theme();
446
447    // Pane background colors are made a little transparent to see the objects behind the setting controls.
448    for token in [SURFACE_PANE_BODY, SURFACE_PANE_HEADER] {
449        if let Some(color) = props.semantic_base.get_mut(&token) {
450            color.set_alpha(0.9);
451        }
452    }
453
454    props
455}
456
457/// Creates the help text at the top of the screen.
458fn add_help_text(commands: &mut Commands) {
459    commands.spawn_scene(bsn! {
460        Node {
461            position_type: PositionType::Absolute,
462            left: px(12),
463            top: px(12),
464        }
465        Children [
466            Text("Drag a setting's input value to change the scene.\n\
467                Click into an input field to change values via keyboard.\n\
468                Values must be between 0 and 10.")
469        ]
470    });
471}