Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/examples/3d/color_grading.rs
6592 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
light::CascadeShadowConfigBuilder,
10
prelude::*,
11
render::view::{ColorGrading, ColorGradingGlobal, ColorGradingSection, Hdr},
12
};
13
use std::fmt::Display;
14
15
static FONT_PATH: &str = "fonts/FiraMono-Medium.ttf";
16
17
/// How quickly the value changes per frame.
18
const OPTION_ADJUSTMENT_SPEED: f32 = 0.003;
19
20
/// The color grading section that the user has selected: highlights, midtones,
21
/// or shadows.
22
#[derive(Clone, Copy, PartialEq)]
23
enum SelectedColorGradingSection {
24
Highlights,
25
Midtones,
26
Shadows,
27
}
28
29
/// The global option that the user has selected.
30
///
31
/// See the documentation of [`ColorGradingGlobal`] for more information about
32
/// each field here.
33
#[derive(Clone, Copy, PartialEq, Default)]
34
enum SelectedGlobalColorGradingOption {
35
#[default]
36
Exposure,
37
Temperature,
38
Tint,
39
Hue,
40
}
41
42
/// The section-specific option that the user has selected.
43
///
44
/// See the documentation of [`ColorGradingSection`] for more information about
45
/// each field here.
46
#[derive(Clone, Copy, PartialEq)]
47
enum SelectedSectionColorGradingOption {
48
Saturation,
49
Contrast,
50
Gamma,
51
Gain,
52
Lift,
53
}
54
55
/// The color grading option that the user has selected.
56
#[derive(Clone, Copy, PartialEq, Resource)]
57
enum SelectedColorGradingOption {
58
/// The user has selected a global color grading option: one that applies to
59
/// the whole image as opposed to specifically to highlights, midtones, or
60
/// shadows.
61
Global(SelectedGlobalColorGradingOption),
62
63
/// The user has selected a color grading option that applies only to
64
/// highlights, midtones, or shadows.
65
Section(
66
SelectedColorGradingSection,
67
SelectedSectionColorGradingOption,
68
),
69
}
70
71
impl Default for SelectedColorGradingOption {
72
fn default() -> Self {
73
Self::Global(default())
74
}
75
}
76
77
/// Buttons consist of three parts: the button itself, a label child, and a
78
/// value child. This specifies one of the three entities.
79
#[derive(Clone, Copy, PartialEq, Component)]
80
enum ColorGradingOptionWidgetType {
81
/// The parent button.
82
Button,
83
/// The label of the button.
84
Label,
85
/// The numerical value that the button displays.
86
Value,
87
}
88
89
#[derive(Clone, Copy, Component)]
90
struct ColorGradingOptionWidget {
91
widget_type: ColorGradingOptionWidgetType,
92
option: SelectedColorGradingOption,
93
}
94
95
/// A marker component for the help text at the top left of the screen.
96
#[derive(Clone, Copy, Component)]
97
struct HelpText;
98
99
fn main() {
100
App::new()
101
.add_plugins(DefaultPlugins)
102
.init_resource::<SelectedColorGradingOption>()
103
.add_systems(Startup, setup)
104
.add_systems(
105
Update,
106
(
107
handle_button_presses,
108
adjust_color_grading_option,
109
update_ui_state,
110
)
111
.chain(),
112
)
113
.run();
114
}
115
116
fn setup(
117
mut commands: Commands,
118
currently_selected_option: Res<SelectedColorGradingOption>,
119
asset_server: Res<AssetServer>,
120
) {
121
// Create the scene.
122
add_basic_scene(&mut commands, &asset_server);
123
124
// Create the root UI element.
125
let font = asset_server.load(FONT_PATH);
126
let color_grading = ColorGrading::default();
127
add_buttons(&mut commands, &font, &color_grading);
128
129
// Spawn help text.
130
add_help_text(&mut commands, &font, &currently_selected_option);
131
132
// Spawn the camera.
133
add_camera(&mut commands, &asset_server, color_grading);
134
}
135
136
/// Adds all the buttons on the bottom of the scene.
137
fn add_buttons(commands: &mut Commands, font: &Handle<Font>, color_grading: &ColorGrading) {
138
commands.spawn((
139
// Spawn the parent node that contains all the buttons.
140
Node {
141
flex_direction: FlexDirection::Column,
142
position_type: PositionType::Absolute,
143
row_gap: px(6),
144
left: px(12),
145
bottom: px(12),
146
..default()
147
},
148
children![
149
// Create the first row, which contains the global controls.
150
buttons_for_global_controls(color_grading, font),
151
// Create the rows for individual controls.
152
buttons_for_section(SelectedColorGradingSection::Highlights, color_grading, font),
153
buttons_for_section(SelectedColorGradingSection::Midtones, color_grading, font),
154
buttons_for_section(SelectedColorGradingSection::Shadows, color_grading, font),
155
],
156
));
157
}
158
159
/// Adds the buttons for the global controls (those that control the scene as a
160
/// whole as opposed to shadows, midtones, or highlights).
161
fn buttons_for_global_controls(color_grading: &ColorGrading, font: &Handle<Font>) -> impl Bundle {
162
let make_button = |option: SelectedGlobalColorGradingOption| {
163
button_for_value(
164
SelectedColorGradingOption::Global(option),
165
color_grading,
166
font,
167
)
168
};
169
170
// Add the parent node for the row.
171
(
172
Node::default(),
173
children![
174
Node {
175
width: px(125),
176
..default()
177
},
178
make_button(SelectedGlobalColorGradingOption::Exposure),
179
make_button(SelectedGlobalColorGradingOption::Temperature),
180
make_button(SelectedGlobalColorGradingOption::Tint),
181
make_button(SelectedGlobalColorGradingOption::Hue),
182
],
183
)
184
}
185
186
/// Adds the buttons that control color grading for individual sections
187
/// (highlights, midtones, shadows).
188
fn buttons_for_section(
189
section: SelectedColorGradingSection,
190
color_grading: &ColorGrading,
191
font: &Handle<Font>,
192
) -> impl Bundle {
193
let make_button = |option| {
194
button_for_value(
195
SelectedColorGradingOption::Section(section, option),
196
color_grading,
197
font,
198
)
199
};
200
201
// Spawn the row container.
202
(
203
Node {
204
align_items: AlignItems::Center,
205
..default()
206
},
207
children![
208
// Spawn the label ("Highlights", etc.)
209
(
210
text(&section.to_string(), font, Color::WHITE),
211
Node {
212
width: px(125),
213
..default()
214
}
215
),
216
// Spawn the buttons.
217
make_button(SelectedSectionColorGradingOption::Saturation),
218
make_button(SelectedSectionColorGradingOption::Contrast),
219
make_button(SelectedSectionColorGradingOption::Gamma),
220
make_button(SelectedSectionColorGradingOption::Gain),
221
make_button(SelectedSectionColorGradingOption::Lift),
222
],
223
)
224
}
225
226
/// Adds a button that controls one of the color grading values.
227
fn button_for_value(
228
option: SelectedColorGradingOption,
229
color_grading: &ColorGrading,
230
font: &Handle<Font>,
231
) -> impl Bundle {
232
let label = match option {
233
SelectedColorGradingOption::Global(option) => option.to_string(),
234
SelectedColorGradingOption::Section(_, option) => option.to_string(),
235
};
236
237
// Add the button node.
238
(
239
Button,
240
Node {
241
border: UiRect::all(px(1)),
242
width: px(200),
243
justify_content: JustifyContent::Center,
244
align_items: AlignItems::Center,
245
padding: UiRect::axes(px(12), px(6)),
246
margin: UiRect::right(px(12)),
247
..default()
248
},
249
BorderColor::all(Color::WHITE),
250
BorderRadius::MAX,
251
BackgroundColor(Color::BLACK),
252
ColorGradingOptionWidget {
253
widget_type: ColorGradingOptionWidgetType::Button,
254
option,
255
},
256
children![
257
// Add the button label.
258
(
259
text(&label, font, Color::WHITE),
260
ColorGradingOptionWidget {
261
widget_type: ColorGradingOptionWidgetType::Label,
262
option,
263
},
264
),
265
// Add a spacer.
266
Node {
267
flex_grow: 1.0,
268
..default()
269
},
270
// Add the value text.
271
(
272
text(
273
&format!("{:.3}", option.get(color_grading)),
274
font,
275
Color::WHITE,
276
),
277
ColorGradingOptionWidget {
278
widget_type: ColorGradingOptionWidgetType::Value,
279
option,
280
},
281
),
282
],
283
)
284
}
285
286
/// Creates the help text at the top of the screen.
287
fn add_help_text(
288
commands: &mut Commands,
289
font: &Handle<Font>,
290
currently_selected_option: &SelectedColorGradingOption,
291
) {
292
commands.spawn((
293
Text::new(create_help_text(currently_selected_option)),
294
TextFont {
295
font: font.clone(),
296
..default()
297
},
298
Node {
299
position_type: PositionType::Absolute,
300
left: px(12),
301
top: px(12),
302
..default()
303
},
304
HelpText,
305
));
306
}
307
308
/// Adds some text to the scene.
309
fn text(label: &str, font: &Handle<Font>, color: Color) -> impl Bundle + use<> {
310
(
311
Text::new(label),
312
TextFont {
313
font: font.clone(),
314
font_size: 15.0,
315
..default()
316
},
317
TextColor(color),
318
)
319
}
320
321
fn add_camera(commands: &mut Commands, asset_server: &AssetServer, color_grading: ColorGrading) {
322
commands.spawn((
323
Camera3d::default(),
324
Hdr,
325
Transform::from_xyz(0.7, 0.7, 1.0).looking_at(Vec3::new(0.0, 0.3, 0.0), Vec3::Y),
326
color_grading,
327
DistanceFog {
328
color: Color::srgb_u8(43, 44, 47),
329
falloff: FogFalloff::Linear {
330
start: 1.0,
331
end: 8.0,
332
},
333
..default()
334
},
335
EnvironmentMapLight {
336
diffuse_map: asset_server.load("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2"),
337
specular_map: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"),
338
intensity: 2000.0,
339
..default()
340
},
341
));
342
}
343
344
fn add_basic_scene(commands: &mut Commands, asset_server: &AssetServer) {
345
// Spawn the main scene.
346
commands.spawn(SceneRoot(asset_server.load(
347
GltfAssetLabel::Scene(0).from_asset("models/TonemappingTest/TonemappingTest.gltf"),
348
)));
349
350
// Spawn the flight helmet.
351
commands.spawn((
352
SceneRoot(
353
asset_server
354
.load(GltfAssetLabel::Scene(0).from_asset("models/FlightHelmet/FlightHelmet.gltf")),
355
),
356
Transform::from_xyz(0.5, 0.0, -0.5).with_rotation(Quat::from_rotation_y(-0.15 * PI)),
357
));
358
359
// Spawn the light.
360
commands.spawn((
361
DirectionalLight {
362
illuminance: 15000.0,
363
shadows_enabled: true,
364
..default()
365
},
366
Transform::from_rotation(Quat::from_euler(EulerRot::ZYX, 0.0, PI * -0.15, PI * -0.15)),
367
CascadeShadowConfigBuilder {
368
maximum_distance: 3.0,
369
first_cascade_far_bound: 0.9,
370
..default()
371
}
372
.build(),
373
));
374
}
375
376
impl Display for SelectedGlobalColorGradingOption {
377
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
378
let name = match *self {
379
SelectedGlobalColorGradingOption::Exposure => "Exposure",
380
SelectedGlobalColorGradingOption::Temperature => "Temperature",
381
SelectedGlobalColorGradingOption::Tint => "Tint",
382
SelectedGlobalColorGradingOption::Hue => "Hue",
383
};
384
f.write_str(name)
385
}
386
}
387
388
impl Display for SelectedColorGradingSection {
389
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
390
let name = match *self {
391
SelectedColorGradingSection::Highlights => "Highlights",
392
SelectedColorGradingSection::Midtones => "Midtones",
393
SelectedColorGradingSection::Shadows => "Shadows",
394
};
395
f.write_str(name)
396
}
397
}
398
399
impl Display for SelectedSectionColorGradingOption {
400
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
401
let name = match *self {
402
SelectedSectionColorGradingOption::Saturation => "Saturation",
403
SelectedSectionColorGradingOption::Contrast => "Contrast",
404
SelectedSectionColorGradingOption::Gamma => "Gamma",
405
SelectedSectionColorGradingOption::Gain => "Gain",
406
SelectedSectionColorGradingOption::Lift => "Lift",
407
};
408
f.write_str(name)
409
}
410
}
411
412
impl Display for SelectedColorGradingOption {
413
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
414
match self {
415
SelectedColorGradingOption::Global(option) => write!(f, "\"{option}\""),
416
SelectedColorGradingOption::Section(section, option) => {
417
write!(f, "\"{option}\" for \"{section}\"")
418
}
419
}
420
}
421
}
422
423
impl SelectedSectionColorGradingOption {
424
/// Returns the appropriate value in the given color grading section.
425
fn get(&self, section: &ColorGradingSection) -> f32 {
426
match *self {
427
SelectedSectionColorGradingOption::Saturation => section.saturation,
428
SelectedSectionColorGradingOption::Contrast => section.contrast,
429
SelectedSectionColorGradingOption::Gamma => section.gamma,
430
SelectedSectionColorGradingOption::Gain => section.gain,
431
SelectedSectionColorGradingOption::Lift => section.lift,
432
}
433
}
434
435
fn set(&self, section: &mut ColorGradingSection, value: f32) {
436
match *self {
437
SelectedSectionColorGradingOption::Saturation => section.saturation = value,
438
SelectedSectionColorGradingOption::Contrast => section.contrast = value,
439
SelectedSectionColorGradingOption::Gamma => section.gamma = value,
440
SelectedSectionColorGradingOption::Gain => section.gain = value,
441
SelectedSectionColorGradingOption::Lift => section.lift = value,
442
}
443
}
444
}
445
446
impl SelectedGlobalColorGradingOption {
447
/// Returns the appropriate value in the given set of global color grading
448
/// values.
449
fn get(&self, global: &ColorGradingGlobal) -> f32 {
450
match *self {
451
SelectedGlobalColorGradingOption::Exposure => global.exposure,
452
SelectedGlobalColorGradingOption::Temperature => global.temperature,
453
SelectedGlobalColorGradingOption::Tint => global.tint,
454
SelectedGlobalColorGradingOption::Hue => global.hue,
455
}
456
}
457
458
/// Sets the appropriate value in the given set of global color grading
459
/// values.
460
fn set(&self, global: &mut ColorGradingGlobal, value: f32) {
461
match *self {
462
SelectedGlobalColorGradingOption::Exposure => global.exposure = value,
463
SelectedGlobalColorGradingOption::Temperature => global.temperature = value,
464
SelectedGlobalColorGradingOption::Tint => global.tint = value,
465
SelectedGlobalColorGradingOption::Hue => global.hue = value,
466
}
467
}
468
}
469
470
impl SelectedColorGradingOption {
471
/// Returns the appropriate value in the given set of color grading values.
472
fn get(&self, color_grading: &ColorGrading) -> f32 {
473
match self {
474
SelectedColorGradingOption::Global(option) => option.get(&color_grading.global),
475
SelectedColorGradingOption::Section(
476
SelectedColorGradingSection::Highlights,
477
option,
478
) => option.get(&color_grading.highlights),
479
SelectedColorGradingOption::Section(SelectedColorGradingSection::Midtones, option) => {
480
option.get(&color_grading.midtones)
481
}
482
SelectedColorGradingOption::Section(SelectedColorGradingSection::Shadows, option) => {
483
option.get(&color_grading.shadows)
484
}
485
}
486
}
487
488
/// Sets the appropriate value in the given set of color grading values.
489
fn set(&self, color_grading: &mut ColorGrading, value: f32) {
490
match self {
491
SelectedColorGradingOption::Global(option) => {
492
option.set(&mut color_grading.global, value);
493
}
494
SelectedColorGradingOption::Section(
495
SelectedColorGradingSection::Highlights,
496
option,
497
) => option.set(&mut color_grading.highlights, value),
498
SelectedColorGradingOption::Section(SelectedColorGradingSection::Midtones, option) => {
499
option.set(&mut color_grading.midtones, value);
500
}
501
SelectedColorGradingOption::Section(SelectedColorGradingSection::Shadows, option) => {
502
option.set(&mut color_grading.shadows, value);
503
}
504
}
505
}
506
}
507
508
/// Handles mouse clicks on the buttons when the user clicks on a new one.
509
fn handle_button_presses(
510
mut interactions: Query<(&Interaction, &ColorGradingOptionWidget), Changed<Interaction>>,
511
mut currently_selected_option: ResMut<SelectedColorGradingOption>,
512
) {
513
for (interaction, widget) in interactions.iter_mut() {
514
if widget.widget_type == ColorGradingOptionWidgetType::Button
515
&& *interaction == Interaction::Pressed
516
{
517
*currently_selected_option = widget.option;
518
}
519
}
520
}
521
522
/// Updates the state of the UI based on the current state.
523
fn update_ui_state(
524
mut buttons: Query<(
525
&mut BackgroundColor,
526
&mut BorderColor,
527
&ColorGradingOptionWidget,
528
)>,
529
button_text: Query<(Entity, &ColorGradingOptionWidget), (With<Text>, Without<HelpText>)>,
530
help_text: Single<Entity, With<HelpText>>,
531
mut writer: TextUiWriter,
532
cameras: Single<Ref<ColorGrading>>,
533
currently_selected_option: Res<SelectedColorGradingOption>,
534
) {
535
// Exit early if the UI didn't change
536
if !currently_selected_option.is_changed() && !cameras.is_changed() {
537
return;
538
}
539
540
// The currently-selected option is drawn with inverted colors.
541
for (mut background, mut border_color, widget) in buttons.iter_mut() {
542
if *currently_selected_option == widget.option {
543
*background = Color::WHITE.into();
544
*border_color = Color::BLACK.into();
545
} else {
546
*background = Color::BLACK.into();
547
*border_color = Color::WHITE.into();
548
}
549
}
550
551
let value_label = format!("{:.3}", currently_selected_option.get(cameras.as_ref()));
552
553
// Update the buttons.
554
for (entity, widget) in button_text.iter() {
555
// Set the text color.
556
557
let color = if *currently_selected_option == widget.option {
558
Color::BLACK
559
} else {
560
Color::WHITE
561
};
562
563
writer.for_each_color(entity, |mut text_color| {
564
text_color.0 = color;
565
});
566
567
// Update the displayed value, if this is the currently-selected option.
568
if widget.widget_type == ColorGradingOptionWidgetType::Value
569
&& *currently_selected_option == widget.option
570
{
571
writer.for_each_text(entity, |mut text| {
572
text.clone_from(&value_label);
573
});
574
}
575
}
576
577
// Update the help text.
578
*writer.text(*help_text, 0) = create_help_text(&currently_selected_option);
579
}
580
581
/// Creates the help text at the top left of the window.
582
fn create_help_text(currently_selected_option: &SelectedColorGradingOption) -> String {
583
format!("Press Left/Right to adjust {currently_selected_option}")
584
}
585
586
/// Processes keyboard input to change the value of the currently-selected color
587
/// grading option.
588
fn adjust_color_grading_option(
589
mut color_grading: Single<&mut ColorGrading>,
590
input: Res<ButtonInput<KeyCode>>,
591
currently_selected_option: Res<SelectedColorGradingOption>,
592
) {
593
let mut delta = 0.0;
594
if input.pressed(KeyCode::ArrowLeft) {
595
delta -= OPTION_ADJUSTMENT_SPEED;
596
}
597
if input.pressed(KeyCode::ArrowRight) {
598
delta += OPTION_ADJUSTMENT_SPEED;
599
}
600
601
if delta != 0.0 {
602
let new_value = currently_selected_option.get(color_grading.as_ref()) + delta;
603
currently_selected_option.set(&mut color_grading, new_value);
604
}
605
}
606
607