1use std::{
4 f32::consts::PI,
5 fmt::{self, Formatter},
6};
7
8use bevy::{
9 light::CascadeShadowConfigBuilder,
10 prelude::*,
11 render::view::{ColorGrading, ColorGradingGlobal, ColorGradingSection, Hdr},
12};
13use std::fmt::Display;
14
15static FONT_PATH: &str = "fonts/FiraMono-Medium.ttf";
16
17const OPTION_ADJUSTMENT_SPEED: f32 = 0.003;
19
20#[derive(Clone, Copy, PartialEq)]
23enum SelectedColorGradingSection {
24 Highlights,
25 Midtones,
26 Shadows,
27}
28
29#[derive(Clone, Copy, PartialEq, Default)]
34enum SelectedGlobalColorGradingOption {
35 #[default]
36 Exposure,
37 Temperature,
38 Tint,
39 Hue,
40}
41
42#[derive(Clone, Copy, PartialEq)]
47enum SelectedSectionColorGradingOption {
48 Saturation,
49 Contrast,
50 Gamma,
51 Gain,
52 Lift,
53}
54
55#[derive(Clone, Copy, PartialEq, Resource)]
57enum SelectedColorGradingOption {
58 Global(SelectedGlobalColorGradingOption),
62
63 Section(
66 SelectedColorGradingSection,
67 SelectedSectionColorGradingOption,
68 ),
69}
70
71impl Default for SelectedColorGradingOption {
72 fn default() -> Self {
73 Self::Global(default())
74 }
75}
76
77#[derive(Clone, Copy, PartialEq, Component)]
80enum ColorGradingOptionWidgetType {
81 Button,
83 Label,
85 Value,
87}
88
89#[derive(Clone, Copy, Component)]
90struct ColorGradingOptionWidget {
91 widget_type: ColorGradingOptionWidgetType,
92 option: SelectedColorGradingOption,
93}
94
95#[derive(Clone, Copy, Component)]
97struct HelpText;
98
99fn 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
116fn setup(
117 mut commands: Commands,
118 currently_selected_option: Res<SelectedColorGradingOption>,
119 asset_server: Res<AssetServer>,
120) {
121 add_basic_scene(&mut commands, &asset_server);
123
124 let font = asset_server.load(FONT_PATH);
126 let color_grading = ColorGrading::default();
127 add_buttons(&mut commands, &font, &color_grading);
128
129 add_help_text(&mut commands, &font, ¤tly_selected_option);
131
132 add_camera(&mut commands, &asset_server, color_grading);
134}
135
136fn add_buttons(commands: &mut Commands, font: &Handle<Font>, color_grading: &ColorGrading) {
138 commands.spawn((
139 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 buttons_for_global_controls(color_grading, font),
151 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
159fn 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 (
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
186fn 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 (
203 Node {
204 align_items: AlignItems::Center,
205 ..default()
206 },
207 children![
208 (
210 text(§ion.to_string(), font, Color::WHITE),
211 Node {
212 width: px(125),
213 ..default()
214 }
215 ),
216 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
226fn 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 (
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 (
259 text(&label, font, Color::WHITE),
260 ColorGradingOptionWidget {
261 widget_type: ColorGradingOptionWidgetType::Label,
262 option,
263 },
264 ),
265 Node {
267 flex_grow: 1.0,
268 ..default()
269 },
270 (
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
286fn 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
308fn 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
321fn 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
344fn add_basic_scene(commands: &mut Commands, asset_server: &AssetServer) {
345 commands.spawn(SceneRoot(asset_server.load(
347 GltfAssetLabel::Scene(0).from_asset("models/TonemappingTest/TonemappingTest.gltf"),
348 )));
349
350 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 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
376impl 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
388impl 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
399impl 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
412impl 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
423impl SelectedSectionColorGradingOption {
424 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
446impl SelectedGlobalColorGradingOption {
447 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 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
470impl SelectedColorGradingOption {
471 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 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
508fn 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
522fn 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 if !currently_selected_option.is_changed() && !cameras.is_changed() {
537 return;
538 }
539
540 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 for (entity, widget) in button_text.iter() {
555 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 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 *writer.text(*help_text, 0) = create_help_text(¤tly_selected_option);
579}
580
581fn create_help_text(currently_selected_option: &SelectedColorGradingOption) -> String {
583 format!("Press Left/Right to adjust {currently_selected_option}")
584}
585
586fn 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}