3D Rendering / Color grading

Back to examples View in GitHub

Support Warning

WebGPU is currently only supported on Chrome starting with version 113, and only on desktop. If they don't work on your configuration, you can check the WebGL2 examples here.

//! Demonstrates color grading with an interactive adjustment UI.

use std::{
    f32::consts::PI,
    fmt::{self, Formatter},
};

use bevy::{
    ecs::system::EntityCommands,
    pbr::CascadeShadowConfigBuilder,
    prelude::*,
    render::view::{ColorGrading, ColorGradingGlobal, ColorGradingSection},
};
use std::fmt::Display;

static FONT_PATH: &str = "fonts/FiraMono-Medium.ttf";

/// How quickly the value changes per frame.
const OPTION_ADJUSTMENT_SPEED: f32 = 0.003;

/// The color grading section that the user has selected: highlights, midtones,
/// or shadows.
#[derive(Clone, Copy, PartialEq)]
enum SelectedColorGradingSection {
    Highlights,
    Midtones,
    Shadows,
}

/// The global option that the user has selected.
///
/// See the documentation of [`ColorGradingGlobal`] for more information about
/// each field here.
#[derive(Clone, Copy, PartialEq, Default)]
enum SelectedGlobalColorGradingOption {
    #[default]
    Exposure,
    Temperature,
    Tint,
    Hue,
}

/// The section-specific option that the user has selected.
///
/// See the documentation of [`ColorGradingSection`] for more information about
/// each field here.
#[derive(Clone, Copy, PartialEq)]
enum SelectedSectionColorGradingOption {
    Saturation,
    Contrast,
    Gamma,
    Gain,
    Lift,
}

/// The color grading option that the user has selected.
#[derive(Clone, Copy, PartialEq, Resource)]
enum SelectedColorGradingOption {
    /// The user has selected a global color grading option: one that applies to
    /// the whole image as opposed to specifically to highlights, midtones, or
    /// shadows.
    Global(SelectedGlobalColorGradingOption),

    /// The user has selected a color grading option that applies only to
    /// highlights, midtones, or shadows.
    Section(
        SelectedColorGradingSection,
        SelectedSectionColorGradingOption,
    ),
}

impl Default for SelectedColorGradingOption {
    fn default() -> Self {
        Self::Global(default())
    }
}

/// Buttons consist of three parts: the button itself, a label child, and a
/// value child. This specifies one of the three entities.
#[derive(Clone, Copy, PartialEq, Component)]
enum ColorGradingOptionWidgetType {
    /// The parent button.
    Button,
    /// The label of the button.
    Label,
    /// The numerical value that the button displays.
    Value,
}

#[derive(Clone, Copy, Component)]
struct ColorGradingOptionWidget {
    widget_type: ColorGradingOptionWidgetType,
    option: SelectedColorGradingOption,
}

/// A marker component for the help text at the top left of the screen.
#[derive(Clone, Copy, Component)]
struct HelpText;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .init_resource::<SelectedColorGradingOption>()
        .add_systems(Startup, setup)
        .add_systems(
            Update,
            (
                handle_button_presses,
                adjust_color_grading_option,
                update_ui_state,
            )
                .chain(),
        )
        .run();
}

fn setup(
    mut commands: Commands,
    currently_selected_option: Res<SelectedColorGradingOption>,
    asset_server: Res<AssetServer>,
) {
    // Create the scene.
    add_basic_scene(&mut commands, &asset_server);

    // Create the root UI element.
    let font = asset_server.load(FONT_PATH);
    let color_grading = ColorGrading::default();
    add_buttons(&mut commands, &font, &color_grading);

    // Spawn help text.
    add_help_text(&mut commands, &font, &currently_selected_option);

    // Spawn the camera.
    add_camera(&mut commands, &asset_server, color_grading);
}

/// Adds all the buttons on the bottom of the scene.
fn add_buttons(commands: &mut Commands, font: &Handle<Font>, color_grading: &ColorGrading) {
    // Spawn the parent node that contains all the buttons.
    commands
        .spawn(Node {
            flex_direction: FlexDirection::Column,
            position_type: PositionType::Absolute,
            row_gap: Val::Px(6.0),
            left: Val::Px(12.0),
            bottom: Val::Px(12.0),
            ..default()
        })
        .with_children(|parent| {
            // Create the first row, which contains the global controls.
            add_buttons_for_global_controls(parent, color_grading, font);

            // Create the rows for individual controls.
            for section in [
                SelectedColorGradingSection::Highlights,
                SelectedColorGradingSection::Midtones,
                SelectedColorGradingSection::Shadows,
            ] {
                add_buttons_for_section(parent, section, color_grading, font);
            }
        });
}

/// Adds the buttons for the global controls (those that control the scene as a
/// whole as opposed to shadows, midtones, or highlights).
fn add_buttons_for_global_controls(
    parent: &mut ChildBuilder,
    color_grading: &ColorGrading,
    font: &Handle<Font>,
) {
    // Add the parent node for the row.
    parent.spawn(Node::default()).with_children(|parent| {
        // Add some placeholder text to fill this column.
        parent.spawn(Node {
            width: Val::Px(125.0),
            ..default()
        });

        // Add each global color grading option button.
        for option in [
            SelectedGlobalColorGradingOption::Exposure,
            SelectedGlobalColorGradingOption::Temperature,
            SelectedGlobalColorGradingOption::Tint,
            SelectedGlobalColorGradingOption::Hue,
        ] {
            add_button_for_value(
                parent,
                SelectedColorGradingOption::Global(option),
                color_grading,
                font,
            );
        }
    });
}

/// Adds the buttons that control color grading for individual sections
/// (highlights, midtones, shadows).
fn add_buttons_for_section(
    parent: &mut ChildBuilder,
    section: SelectedColorGradingSection,
    color_grading: &ColorGrading,
    font: &Handle<Font>,
) {
    // Spawn the row container.
    parent
        .spawn(Node {
            align_items: AlignItems::Center,
            ..default()
        })
        .with_children(|parent| {
            // Spawn the label ("Highlights", etc.)
            add_text(parent, &section.to_string(), font, Color::WHITE).insert(Node {
                width: Val::Px(125.0),
                ..default()
            });

            // Spawn the buttons.
            for option in [
                SelectedSectionColorGradingOption::Saturation,
                SelectedSectionColorGradingOption::Contrast,
                SelectedSectionColorGradingOption::Gamma,
                SelectedSectionColorGradingOption::Gain,
                SelectedSectionColorGradingOption::Lift,
            ] {
                add_button_for_value(
                    parent,
                    SelectedColorGradingOption::Section(section, option),
                    color_grading,
                    font,
                );
            }
        });
}

/// Adds a button that controls one of the color grading values.
fn add_button_for_value(
    parent: &mut ChildBuilder,
    option: SelectedColorGradingOption,
    color_grading: &ColorGrading,
    font: &Handle<Font>,
) {
    // Add the button node.
    parent
        .spawn((
            Button,
            Node {
                border: UiRect::all(Val::Px(1.0)),
                width: Val::Px(200.0),
                justify_content: JustifyContent::Center,
                align_items: AlignItems::Center,
                padding: UiRect::axes(Val::Px(12.0), Val::Px(6.0)),
                margin: UiRect::right(Val::Px(12.0)),
                ..default()
            },
            BorderColor(Color::WHITE),
            BorderRadius::MAX,
            BackgroundColor(Color::BLACK),
        ))
        .insert(ColorGradingOptionWidget {
            widget_type: ColorGradingOptionWidgetType::Button,
            option,
        })
        .with_children(|parent| {
            // Add the button label.
            let label = match option {
                SelectedColorGradingOption::Global(option) => option.to_string(),
                SelectedColorGradingOption::Section(_, option) => option.to_string(),
            };
            add_text(parent, &label, font, Color::WHITE).insert(ColorGradingOptionWidget {
                widget_type: ColorGradingOptionWidgetType::Label,
                option,
            });

            // Add a spacer.
            parent.spawn(Node {
                flex_grow: 1.0,
                ..default()
            });

            // Add the value text.
            add_text(
                parent,
                &format!("{:.3}", option.get(color_grading)),
                font,
                Color::WHITE,
            )
            .insert(ColorGradingOptionWidget {
                widget_type: ColorGradingOptionWidgetType::Value,
                option,
            });
        });
}

/// Creates the help text at the top of the screen.
fn add_help_text(
    commands: &mut Commands,
    font: &Handle<Font>,
    currently_selected_option: &SelectedColorGradingOption,
) {
    commands.spawn((
        Text::new(create_help_text(currently_selected_option)),
        TextFont {
            font: font.clone(),
            ..default()
        },
        Node {
            position_type: PositionType::Absolute,
            left: Val::Px(12.0),
            top: Val::Px(12.0),
            ..default()
        },
        HelpText,
    ));
}

/// Adds some text to the scene.
fn add_text<'a>(
    parent: &'a mut ChildBuilder,
    label: &str,
    font: &Handle<Font>,
    color: Color,
) -> EntityCommands<'a> {
    parent.spawn((
        Text::new(label),
        TextFont {
            font: font.clone(),
            font_size: 15.0,
            ..default()
        },
        TextColor(color),
    ))
}

fn add_camera(commands: &mut Commands, asset_server: &AssetServer, color_grading: ColorGrading) {
    commands.spawn((
        Camera3d::default(),
        Camera {
            hdr: true,
            ..default()
        },
        Transform::from_xyz(0.7, 0.7, 1.0).looking_at(Vec3::new(0.0, 0.3, 0.0), Vec3::Y),
        color_grading,
        DistanceFog {
            color: Color::srgb_u8(43, 44, 47),
            falloff: FogFalloff::Linear {
                start: 1.0,
                end: 8.0,
            },
            ..default()
        },
        EnvironmentMapLight {
            diffuse_map: asset_server.load("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2"),
            specular_map: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"),
            intensity: 2000.0,
            ..default()
        },
    ));
}

fn add_basic_scene(commands: &mut Commands, asset_server: &AssetServer) {
    // Spawn the main scene.
    commands.spawn(SceneRoot(asset_server.load(
        GltfAssetLabel::Scene(0).from_asset("models/TonemappingTest/TonemappingTest.gltf"),
    )));

    // Spawn the flight helmet.
    commands.spawn((
        SceneRoot(
            asset_server
                .load(GltfAssetLabel::Scene(0).from_asset("models/FlightHelmet/FlightHelmet.gltf")),
        ),
        Transform::from_xyz(0.5, 0.0, -0.5).with_rotation(Quat::from_rotation_y(-0.15 * PI)),
    ));

    // Spawn the light.
    commands.spawn((
        DirectionalLight {
            illuminance: 15000.0,
            shadows_enabled: true,
            ..default()
        },
        Transform::from_rotation(Quat::from_euler(EulerRot::ZYX, 0.0, PI * -0.15, PI * -0.15)),
        CascadeShadowConfigBuilder {
            maximum_distance: 3.0,
            first_cascade_far_bound: 0.9,
            ..default()
        }
        .build(),
    ));
}

impl Display for SelectedGlobalColorGradingOption {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        let name = match *self {
            SelectedGlobalColorGradingOption::Exposure => "Exposure",
            SelectedGlobalColorGradingOption::Temperature => "Temperature",
            SelectedGlobalColorGradingOption::Tint => "Tint",
            SelectedGlobalColorGradingOption::Hue => "Hue",
        };
        f.write_str(name)
    }
}

impl Display for SelectedColorGradingSection {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        let name = match *self {
            SelectedColorGradingSection::Highlights => "Highlights",
            SelectedColorGradingSection::Midtones => "Midtones",
            SelectedColorGradingSection::Shadows => "Shadows",
        };
        f.write_str(name)
    }
}

impl Display for SelectedSectionColorGradingOption {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        let name = match *self {
            SelectedSectionColorGradingOption::Saturation => "Saturation",
            SelectedSectionColorGradingOption::Contrast => "Contrast",
            SelectedSectionColorGradingOption::Gamma => "Gamma",
            SelectedSectionColorGradingOption::Gain => "Gain",
            SelectedSectionColorGradingOption::Lift => "Lift",
        };
        f.write_str(name)
    }
}

impl Display for SelectedColorGradingOption {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        match self {
            SelectedColorGradingOption::Global(option) => write!(f, "\"{option}\""),
            SelectedColorGradingOption::Section(section, option) => {
                write!(f, "\"{option}\" for \"{section}\"")
            }
        }
    }
}

impl SelectedSectionColorGradingOption {
    /// Returns the appropriate value in the given color grading section.
    fn get(&self, section: &ColorGradingSection) -> f32 {
        match *self {
            SelectedSectionColorGradingOption::Saturation => section.saturation,
            SelectedSectionColorGradingOption::Contrast => section.contrast,
            SelectedSectionColorGradingOption::Gamma => section.gamma,
            SelectedSectionColorGradingOption::Gain => section.gain,
            SelectedSectionColorGradingOption::Lift => section.lift,
        }
    }

    fn set(&self, section: &mut ColorGradingSection, value: f32) {
        match *self {
            SelectedSectionColorGradingOption::Saturation => section.saturation = value,
            SelectedSectionColorGradingOption::Contrast => section.contrast = value,
            SelectedSectionColorGradingOption::Gamma => section.gamma = value,
            SelectedSectionColorGradingOption::Gain => section.gain = value,
            SelectedSectionColorGradingOption::Lift => section.lift = value,
        }
    }
}

impl SelectedGlobalColorGradingOption {
    /// Returns the appropriate value in the given set of global color grading
    /// values.
    fn get(&self, global: &ColorGradingGlobal) -> f32 {
        match *self {
            SelectedGlobalColorGradingOption::Exposure => global.exposure,
            SelectedGlobalColorGradingOption::Temperature => global.temperature,
            SelectedGlobalColorGradingOption::Tint => global.tint,
            SelectedGlobalColorGradingOption::Hue => global.hue,
        }
    }

    /// Sets the appropriate value in the given set of global color grading
    /// values.
    fn set(&self, global: &mut ColorGradingGlobal, value: f32) {
        match *self {
            SelectedGlobalColorGradingOption::Exposure => global.exposure = value,
            SelectedGlobalColorGradingOption::Temperature => global.temperature = value,
            SelectedGlobalColorGradingOption::Tint => global.tint = value,
            SelectedGlobalColorGradingOption::Hue => global.hue = value,
        }
    }
}

impl SelectedColorGradingOption {
    /// Returns the appropriate value in the given set of color grading values.
    fn get(&self, color_grading: &ColorGrading) -> f32 {
        match self {
            SelectedColorGradingOption::Global(option) => option.get(&color_grading.global),
            SelectedColorGradingOption::Section(
                SelectedColorGradingSection::Highlights,
                option,
            ) => option.get(&color_grading.highlights),
            SelectedColorGradingOption::Section(SelectedColorGradingSection::Midtones, option) => {
                option.get(&color_grading.midtones)
            }
            SelectedColorGradingOption::Section(SelectedColorGradingSection::Shadows, option) => {
                option.get(&color_grading.shadows)
            }
        }
    }

    /// Sets the appropriate value in the given set of color grading values.
    fn set(&self, color_grading: &mut ColorGrading, value: f32) {
        match self {
            SelectedColorGradingOption::Global(option) => {
                option.set(&mut color_grading.global, value);
            }
            SelectedColorGradingOption::Section(
                SelectedColorGradingSection::Highlights,
                option,
            ) => option.set(&mut color_grading.highlights, value),
            SelectedColorGradingOption::Section(SelectedColorGradingSection::Midtones, option) => {
                option.set(&mut color_grading.midtones, value);
            }
            SelectedColorGradingOption::Section(SelectedColorGradingSection::Shadows, option) => {
                option.set(&mut color_grading.shadows, value);
            }
        }
    }
}

/// Handles mouse clicks on the buttons when the user clicks on a new one.
fn handle_button_presses(
    mut interactions: Query<(&Interaction, &ColorGradingOptionWidget), Changed<Interaction>>,
    mut currently_selected_option: ResMut<SelectedColorGradingOption>,
) {
    for (interaction, widget) in interactions.iter_mut() {
        if widget.widget_type == ColorGradingOptionWidgetType::Button
            && *interaction == Interaction::Pressed
        {
            *currently_selected_option = widget.option;
        }
    }
}

/// Updates the state of the UI based on the current state.
fn update_ui_state(
    mut buttons: Query<(
        &mut BackgroundColor,
        &mut BorderColor,
        &ColorGradingOptionWidget,
    )>,
    button_text: Query<(Entity, &ColorGradingOptionWidget), (With<Text>, Without<HelpText>)>,
    help_text: Single<Entity, With<HelpText>>,
    mut writer: TextUiWriter,
    cameras: Single<Ref<ColorGrading>>,
    currently_selected_option: Res<SelectedColorGradingOption>,
) {
    // Exit early if the UI didn't change
    if !currently_selected_option.is_changed() && !cameras.is_changed() {
        return;
    }

    // The currently-selected option is drawn with inverted colors.
    for (mut background, mut border_color, widget) in buttons.iter_mut() {
        if *currently_selected_option == widget.option {
            *background = Color::WHITE.into();
            *border_color = Color::BLACK.into();
        } else {
            *background = Color::BLACK.into();
            *border_color = Color::WHITE.into();
        }
    }

    let value_label = format!("{:.3}", currently_selected_option.get(cameras.as_ref()));

    // Update the buttons.
    for (entity, widget) in button_text.iter() {
        // Set the text color.

        let color = if *currently_selected_option == widget.option {
            Color::BLACK
        } else {
            Color::WHITE
        };

        writer.for_each_color(entity, |mut text_color| {
            text_color.0 = color;
        });

        // Update the displayed value, if this is the currently-selected option.
        if widget.widget_type == ColorGradingOptionWidgetType::Value
            && *currently_selected_option == widget.option
        {
            writer.for_each_text(entity, |mut text| {
                text.clone_from(&value_label);
            });
        }
    }

    // Update the help text.
    *writer.text(*help_text, 0) = create_help_text(&currently_selected_option);
}

/// Creates the help text at the top left of the window.
fn create_help_text(currently_selected_option: &SelectedColorGradingOption) -> String {
    format!("Press Left/Right to adjust {currently_selected_option}")
}

/// Processes keyboard input to change the value of the currently-selected color
/// grading option.
fn adjust_color_grading_option(
    mut color_grading: Single<&mut ColorGrading>,
    input: Res<ButtonInput<KeyCode>>,
    currently_selected_option: Res<SelectedColorGradingOption>,
) {
    let mut delta = 0.0;
    if input.pressed(KeyCode::ArrowLeft) {
        delta -= OPTION_ADJUSTMENT_SPEED;
    }
    if input.pressed(KeyCode::ArrowRight) {
        delta += OPTION_ADJUSTMENT_SPEED;
    }

    if delta != 0.0 {
        let new_value = currently_selected_option.get(color_grading.as_ref()) + delta;
        currently_selected_option.set(&mut color_grading, new_value);
    }
}