This example is running in WebGL2 and should work in most browsers. You can check the WebGPU 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, ¤tly_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, §ion.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(¤tly_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);
}
}