3D Rendering / Color grading

Back to examples View in GitHub

Demonstrates color grading with an interactive adjustment UI.

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

use bevy::{
    camera::Hdr,
    feathers::{
        containers::{pane, pane_body, pane_header},
        controls::{FeathersNumberInput, HardLimit, NumberInputPrecision, NumberInputValue},
        dark_theme::create_dark_theme,
        display::label,
        theme::{ThemeProps, UiTheme},
        tokens::semantic::{SURFACE_PANE_BODY, SURFACE_PANE_HEADER},
        FeathersPlugins,
    },
    light::CascadeShadowConfigBuilder,
    prelude::*,
    render::view::{ColorGrading, ColorGradingGlobal, ColorGradingSection},
    ui_widgets::ValueChange,
};
use std::fmt::Display;

/// The global color grading settings that the user can modify.
///
/// See the documentation of [`ColorGradingGlobal`] for more information about
/// each field here.
#[derive(Clone, Copy, PartialEq, Default)]
enum GlobalColorGradingSetting {
    #[default]
    Exposure,
    Temperature,
    Tint,
    Hue,
}

/// A color grading section that the user can modify the settings of:
/// highlights, midtones, or shadows.
#[derive(Clone, Copy, PartialEq, Default)]
enum SectionColorGradingName {
    #[default]
    Highlights,
    Midtones,
    Shadows,
}

/// The section-specific color grading setting that the user can modify.
///
/// See the documentation of [`ColorGradingSection`] for more information about
/// each field here.
#[derive(Clone, Copy, PartialEq, Default)]
enum SectionColorGradingSetting {
    #[default]
    Saturation,
    Contrast,
    Gamma,
    Gain,
    Lift,
}

/// A color grading settings that the user can modify.
#[derive(Component, Clone, Copy, PartialEq)]
enum ColorGradingSetting {
    /// The global color grading settings. They apply to
    /// the whole image as opposed to specifically to highlights, midtones, or
    /// shadows.
    Global(GlobalColorGradingSetting),

    /// A color grading setting that applies only to highlights, midtones, or shadows.
    Section(SectionColorGradingName, SectionColorGradingSetting),
}

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

fn main() {
    App::new()
        .add_plugins((DefaultPlugins, FeathersPlugins))
        .insert_resource(UiTheme(get_example_theme()))
        .add_systems(Startup, setup)
        .add_observer(handle_value_change_number_input)
        .run();
}

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

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

    add_help_text(&mut commands);

    // 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, color_grading: &ColorGrading) {
    commands.spawn_scene(bsn! {
        // Spawn the parent node that contains all the buttons.
        Node {
            flex_direction: FlexDirection::Row,
            position_type: PositionType::Absolute,
            column_gap: px(6),
            left: px(12),
            bottom: px(12),
        }
        Children [
            // Create the first pane, which contains the global controls.
            @pane_for_global_controls(color_grading)
            --
            // Create the following panes for individual controls.
            @pane_for_section(SectionColorGradingName::Highlights, color_grading)
            --
            @pane_for_section(SectionColorGradingName::Midtones, color_grading)
            --
            @pane_for_section(SectionColorGradingName::Shadows, color_grading)
        ]
    });
}

/// Adds the buttons for the global controls (those that control the scene as a
/// whole as opposed to shadows, midtones, or highlights).
fn pane_for_global_controls(color_grading: &ColorGrading) -> impl Scene {
    let make_button =
        |option| number_input_for_value(ColorGradingSetting::Global(option), color_grading);

    bsn! {
        @pane()
        Children [
            // Spawn the label ("Highlights", etc.)
            @pane_header()
            Children[
                Node {
                    width: px(120)
                    align_self: AlignSelf::Start,
                }
                Children [
                    @label("Global Settings")
                ]
            ]
            --
            // Spawn the buttons
            @pane_body()
            Children [
                @make_button(GlobalColorGradingSetting::Exposure)
                --
                @make_button(GlobalColorGradingSetting::Temperature)
                --
                @make_button(GlobalColorGradingSetting::Tint)
                --
                @make_button(GlobalColorGradingSetting::Hue)
            ]
        ]
    }
}

/// Adds the buttons that control color grading for individual sections
/// (highlights, midtones, shadows).
fn pane_for_section(section: SectionColorGradingName, color_grading: &ColorGrading) -> impl Scene {
    let make_button = |setting| {
        number_input_for_value(
            ColorGradingSetting::Section(section, setting),
            color_grading,
        )
    };

    bsn! {
        @pane()
        Children [
            // Spawn the label ("Highlights", etc.)
            @pane_header() Children [
                Node {
                    width: px(120),
                    align_self: AlignSelf::Start,
                }
                Children [
                    @label(section.to_string())
                ]
            ]
            --
            // Spawn the buttons.
            @pane_body() Children[
                @make_button(SectionColorGradingSetting::Saturation)
                --
                @make_button(SectionColorGradingSetting::Contrast)
                --
                @make_button(SectionColorGradingSetting::Gamma)
                --
                @make_button(SectionColorGradingSetting::Gain)
                --
                @make_button(SectionColorGradingSetting::Lift)
            ]
        ]
    }
}

/// Adds a feathers number input that controls one of the color grading values.
fn number_input_for_value(
    setting: ColorGradingSetting,
    color_grading: &ColorGrading,
) -> impl Scene {
    let setting_label = match setting {
        ColorGradingSetting::Global(setting) => setting.to_string(),
        ColorGradingSetting::Section(_, setting) => setting.to_string(),
    };

    bsn! {
        Node {
            justify_content: JustifyContent::Center,
            align_items: AlignItems::Center,
        }
        Children [
            Node {
                width: px(120),
            }
            Children[
                @label(setting_label)
            ]
            --
            Node {
                align_items: AlignItems::Center,
                width: px(50),
            }
            @FeathersNumberInput
            NumberInputValue::F32({setting.get(color_grading)})
            setting
            NumberInputPrecision(2)
            HardLimit::f32(0. ..=10.)
        ]
    }
}

fn add_camera(commands: &mut Commands, asset_server: &AssetServer, color_grading: ColorGrading) {
    commands.spawn((
        Camera3d::default(),
        Hdr,
        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(WorldAssetRoot(asset_server.load(
        GltfAssetLabel::Scene(0).from_asset("models/TonemappingTest/TonemappingTest.gltf"),
    )));

    // Spawn the flight helmet.
    commands.spawn((
        WorldAssetRoot(
            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,
            shadow_maps_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(),
    ));
}

/// Observer that handles changes to number inputs.
fn handle_value_change_number_input(
    value_change: On<ValueChange<f32>>,
    mut commands: Commands,
    setting_q: Query<&ColorGradingSetting, With<FeathersNumberInput>>,
    mut color_grading: Single<&mut ColorGrading>,
) {
    if let Ok(setting) = setting_q.get(value_change.source) {
        setting.set(&mut color_grading, value_change.value);

        commands
            .entity(value_change.source)
            .insert(NumberInputValue::F32(value_change.value));
    }
}

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

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

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

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

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

    /// Sets the appropriate value in the given set of color grading values.
    fn set(&self, section: &mut ColorGradingSection, value: f32) {
        match *self {
            SectionColorGradingSetting::Saturation => section.saturation = value,
            SectionColorGradingSetting::Contrast => section.contrast = value,
            SectionColorGradingSetting::Gamma => section.gamma = value,
            SectionColorGradingSetting::Gain => section.gain = value,
            SectionColorGradingSetting::Lift => section.lift = value,
        }
    }
}

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

impl ColorGradingSetting {
    /// Returns the appropriate value in the given set of color grading values.
    fn get(&self, color_grading: &ColorGrading) -> f32 {
        match self {
            ColorGradingSetting::Global(option) => option.get(&color_grading.global),
            ColorGradingSetting::Section(SectionColorGradingName::Highlights, option) => {
                option.get(&color_grading.highlights)
            }
            ColorGradingSetting::Section(SectionColorGradingName::Midtones, option) => {
                option.get(&color_grading.midtones)
            }
            ColorGradingSetting::Section(SectionColorGradingName::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 {
            ColorGradingSetting::Global(option) => {
                option.set(&mut color_grading.global, value);
            }
            ColorGradingSetting::Section(SectionColorGradingName::Highlights, option) => {
                option.set(&mut color_grading.highlights, value);
            }
            ColorGradingSetting::Section(SectionColorGradingName::Midtones, option) => {
                option.set(&mut color_grading.midtones, value);
            }
            ColorGradingSetting::Section(SectionColorGradingName::Shadows, option) => {
                option.set(&mut color_grading.shadows, value);
            }
        }
    }
}

/// Get the Feathers Theme for the example. It is a slightly modified dark theme.
fn get_example_theme() -> ThemeProps {
    let mut props = create_dark_theme();

    // Pane background colors are made a little transparent to see the objects behind the setting controls.
    for token in [SURFACE_PANE_BODY, SURFACE_PANE_HEADER] {
        if let Some(color) = props.semantic_base.get_mut(&token) {
            color.set_alpha(0.9);
        }
    }

    props
}

/// Creates the help text at the top of the screen.
fn add_help_text(commands: &mut Commands) {
    commands.spawn_scene(bsn! {
        Node {
            position_type: PositionType::Absolute,
            left: px(12),
            top: px(12),
        }
        Children [
            Text("Drag a setting's input value to change the scene.\n\
                Click into an input field to change values via keyboard.\n\
                Values must be between 0 and 10.")
        ]
    });
}