1use std::{
4 f32::consts::PI,
5 fmt::{self, Formatter},
6};
7
8use bevy::{
9 camera::Hdr,
10 light::CascadeShadowConfigBuilder,
11 prelude::*,
12 render::view::{ColorGrading, ColorGradingGlobal, ColorGradingSection},
13};
14use std::fmt::Display;
15
16static FONT_PATH: &str = "fonts/FiraMono-Medium.ttf";
17
18const OPTION_ADJUSTMENT_SPEED: f32 = 0.003;
20
21#[derive(Clone, Copy, PartialEq)]
24enum SelectedColorGradingSection {
25 Highlights,
26 Midtones,
27 Shadows,
28}
29
30#[derive(Clone, Copy, PartialEq, Default)]
35enum SelectedGlobalColorGradingOption {
36 #[default]
37 Exposure,
38 Temperature,
39 Tint,
40 Hue,
41}
42
43#[derive(Clone, Copy, PartialEq)]
48enum SelectedSectionColorGradingOption {
49 Saturation,
50 Contrast,
51 Gamma,
52 Gain,
53 Lift,
54}
55
56#[derive(Clone, Copy, PartialEq, Resource)]
58enum SelectedColorGradingOption {
59 Global(SelectedGlobalColorGradingOption),
63
64 Section(
67 SelectedColorGradingSection,
68 SelectedSectionColorGradingOption,
69 ),
70}
71
72impl Default for SelectedColorGradingOption {
73 fn default() -> Self {
74 Self::Global(default())
75 }
76}
77
78#[derive(Clone, Copy, PartialEq, Component)]
81enum ColorGradingOptionWidgetType {
82 Button,
84 Label,
86 Value,
88}
89
90#[derive(Clone, Copy, Component)]
91struct ColorGradingOptionWidget {
92 widget_type: ColorGradingOptionWidgetType,
93 option: SelectedColorGradingOption,
94}
95
96#[derive(Clone, Copy, Component)]
98struct HelpText;
99
100fn 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
117fn setup(
118 mut commands: Commands,
119 currently_selected_option: Res<SelectedColorGradingOption>,
120 asset_server: Res<AssetServer>,
121) {
122 add_basic_scene(&mut commands, &asset_server);
124
125 let font = asset_server.load(FONT_PATH);
127 let color_grading = ColorGrading::default();
128 add_buttons(&mut commands, &font, &color_grading);
129
130 add_help_text(&mut commands, &font, ¤tly_selected_option);
132
133 add_camera(&mut commands, &asset_server, color_grading);
135}
136
137fn add_buttons(commands: &mut Commands, font: &Handle<Font>, color_grading: &ColorGrading) {
139 commands.spawn((
140 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 buttons_for_global_controls(color_grading, font),
152 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
160fn 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 (
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
187fn 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 (
204 Node {
205 align_items: AlignItems::Center,
206 ..default()
207 },
208 children![
209 (
211 text(§ion.to_string(), font, Color::WHITE),
212 Node {
213 width: px(125),
214 ..default()
215 }
216 ),
217 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
227fn 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 (
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 (
260 text(&label, font, Color::WHITE),
261 ColorGradingOptionWidget {
262 widget_type: ColorGradingOptionWidgetType::Label,
263 option,
264 },
265 ),
266 Node {
268 flex_grow: 1.0,
269 ..default()
270 },
271 (
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
287fn 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
309fn 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
322fn 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
345fn add_basic_scene(commands: &mut Commands, asset_server: &AssetServer) {
346 commands.spawn(WorldAssetRoot(asset_server.load(
348 GltfAssetLabel::Scene(0).from_asset("models/TonemappingTest/TonemappingTest.gltf"),
349 )));
350
351 commands.spawn((
353 WorldAssetRoot(
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 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
377impl 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
389impl 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
400impl 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
413impl 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
424impl SelectedSectionColorGradingOption {
425 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
447impl SelectedGlobalColorGradingOption {
448 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 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
471impl SelectedColorGradingOption {
472 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 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
509fn 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
523fn 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 if !currently_selected_option.is_changed() && !cameras.is_changed() {
538 return;
539 }
540
541 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 for (entity, widget) in button_text.iter() {
556 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 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 *writer.text(*help_text, 0) = create_help_text(¤tly_selected_option);
580}
581
582fn create_help_text(currently_selected_option: &SelectedColorGradingOption) -> String {
584 format!("Press Left/Right to adjust {currently_selected_option}")
585}
586
587fn 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}