Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/examples/3d/color_grading.rs
9331 views
1
//! Demonstrates color grading with an interactive adjustment UI.
2
3
use std::{
4
f32::consts::PI,
5
fmt::{self, Formatter},
6
};
7
8
use bevy::{
9
camera::Hdr,
10
light::CascadeShadowConfigBuilder,
11
prelude::*,
12
render::view::{ColorGrading, ColorGradingGlobal, ColorGradingSection},
13
};
14
use std::fmt::Display;
15
16
static FONT_PATH: &str = "fonts/FiraMono-Medium.ttf";
17
18
/// How quickly the value changes per frame.
19
const 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)]
24
enum 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)]
35
enum 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)]
48
enum 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)]
58
enum 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
72
impl 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)]
81
enum 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)]
91
struct 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)]
98
struct HelpText;
99
100
fn 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
117
fn 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.
138
fn 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).
162
fn 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).
189
fn 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.
228
fn 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.
288
fn 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.
310
fn 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
322
fn 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
345
fn add_basic_scene(commands: &mut Commands, asset_server: &AssetServer) {
346
// Spawn the main scene.
347
commands.spawn(SceneRoot(asset_server.load(
348
GltfAssetLabel::Scene(0).from_asset("models/TonemappingTest/TonemappingTest.gltf"),
349
)));
350
351
// Spawn the flight helmet.
352
commands.spawn((
353
SceneRoot(
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
377
impl 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
389
impl 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
400
impl 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
413
impl 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
424
impl 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
447
impl 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
471
impl 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.
510
fn 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.
524
fn 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.
583
fn 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.
589
fn 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
}
607
608