1use std::{
4 f32::consts::PI,
5 fmt::{self, Formatter},
6};
7
8use bevy::{
9 camera::Hdr,
10 feathers::{
11 containers::{pane, pane_body, pane_header},
12 controls::{FeathersNumberInput, HardLimit, NumberInputPrecision, NumberInputValue},
13 dark_theme::create_dark_theme,
14 display::label,
15 theme::{ThemeProps, UiTheme},
16 tokens::semantic::{SURFACE_PANE_BODY, SURFACE_PANE_HEADER},
17 FeathersPlugins,
18 },
19 light::CascadeShadowConfigBuilder,
20 prelude::*,
21 render::view::{ColorGrading, ColorGradingGlobal, ColorGradingSection},
22 ui_widgets::ValueChange,
23};
24use std::fmt::Display;
25
26#[derive(Clone, Copy, PartialEq, Default)]
31enum GlobalColorGradingSetting {
32 #[default]
33 Exposure,
34 Temperature,
35 Tint,
36 Hue,
37}
38
39#[derive(Clone, Copy, PartialEq, Default)]
42enum SectionColorGradingName {
43 #[default]
44 Highlights,
45 Midtones,
46 Shadows,
47}
48
49#[derive(Clone, Copy, PartialEq, Default)]
54enum SectionColorGradingSetting {
55 #[default]
56 Saturation,
57 Contrast,
58 Gamma,
59 Gain,
60 Lift,
61}
62
63#[derive(Component, Clone, Copy, PartialEq)]
65enum ColorGradingSetting {
66 Global(GlobalColorGradingSetting),
70
71 Section(SectionColorGradingName, SectionColorGradingSetting),
73}
74
75impl Default for ColorGradingSetting {
76 fn default() -> Self {
77 Self::Global(default())
78 }
79}
80
81fn main() {
82 App::new()
83 .add_plugins((DefaultPlugins, FeathersPlugins))
84 .insert_resource(UiTheme(get_example_theme()))
85 .add_systems(Startup, setup)
86 .add_observer(handle_value_change_number_input)
87 .run();
88}
89
90fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
91 add_basic_scene(&mut commands, &asset_server);
93
94 let color_grading = ColorGrading::default();
96 add_buttons(&mut commands, &color_grading);
97
98 add_help_text(&mut commands);
99
100 add_camera(&mut commands, &asset_server, color_grading);
102}
103
104fn add_buttons(commands: &mut Commands, color_grading: &ColorGrading) {
106 commands.spawn_scene(bsn! {
107 Node {
109 flex_direction: FlexDirection::Row,
110 position_type: PositionType::Absolute,
111 column_gap: px(6),
112 left: px(12),
113 bottom: px(12),
114 }
115 Children [
116 @pane_for_global_controls(color_grading)
118 --
119 @pane_for_section(SectionColorGradingName::Highlights, color_grading)
121 --
122 @pane_for_section(SectionColorGradingName::Midtones, color_grading)
123 --
124 @pane_for_section(SectionColorGradingName::Shadows, color_grading)
125 ]
126 });
127}
128
129fn pane_for_global_controls(color_grading: &ColorGrading) -> impl Scene {
132 let make_button =
133 |option| number_input_for_value(ColorGradingSetting::Global(option), color_grading);
134
135 bsn! {
136 @pane()
137 Children [
138 @pane_header()
140 Children[
141 Node {
142 width: px(120)
143 align_self: AlignSelf::Start,
144 }
145 Children [
146 @label("Global Settings")
147 ]
148 ]
149 --
150 @pane_body()
152 Children [
153 @make_button(GlobalColorGradingSetting::Exposure)
154 --
155 @make_button(GlobalColorGradingSetting::Temperature)
156 --
157 @make_button(GlobalColorGradingSetting::Tint)
158 --
159 @make_button(GlobalColorGradingSetting::Hue)
160 ]
161 ]
162 }
163}
164
165fn pane_for_section(section: SectionColorGradingName, color_grading: &ColorGrading) -> impl Scene {
168 let make_button = |setting| {
169 number_input_for_value(
170 ColorGradingSetting::Section(section, setting),
171 color_grading,
172 )
173 };
174
175 bsn! {
176 @pane()
177 Children [
178 @pane_header() Children [
180 Node {
181 width: px(120),
182 align_self: AlignSelf::Start,
183 }
184 Children [
185 @label(section.to_string())
186 ]
187 ]
188 --
189 @pane_body() Children[
191 @make_button(SectionColorGradingSetting::Saturation)
192 --
193 @make_button(SectionColorGradingSetting::Contrast)
194 --
195 @make_button(SectionColorGradingSetting::Gamma)
196 --
197 @make_button(SectionColorGradingSetting::Gain)
198 --
199 @make_button(SectionColorGradingSetting::Lift)
200 ]
201 ]
202 }
203}
204
205fn number_input_for_value(
207 setting: ColorGradingSetting,
208 color_grading: &ColorGrading,
209) -> impl Scene {
210 let setting_label = match setting {
211 ColorGradingSetting::Global(setting) => setting.to_string(),
212 ColorGradingSetting::Section(_, setting) => setting.to_string(),
213 };
214
215 bsn! {
216 Node {
217 justify_content: JustifyContent::Center,
218 align_items: AlignItems::Center,
219 }
220 Children [
221 Node {
222 width: px(120),
223 }
224 Children[
225 @label(setting_label)
226 ]
227 --
228 Node {
229 align_items: AlignItems::Center,
230 width: px(50),
231 }
232 @FeathersNumberInput
233 NumberInputValue::F32({setting.get(color_grading)})
234 setting
235 NumberInputPrecision(2)
236 HardLimit::f32(0. ..=10.)
237 ]
238 }
239}
240
241fn add_camera(commands: &mut Commands, asset_server: &AssetServer, color_grading: ColorGrading) {
242 commands.spawn((
243 Camera3d::default(),
244 Hdr,
245 Transform::from_xyz(0.7, 0.7, 1.0).looking_at(Vec3::new(0.0, 0.3, 0.0), Vec3::Y),
246 color_grading,
247 DistanceFog {
248 color: Color::srgb_u8(43, 44, 47),
249 falloff: FogFalloff::Linear {
250 start: 1.0,
251 end: 8.0,
252 },
253 ..default()
254 },
255 EnvironmentMapLight {
256 diffuse_map: asset_server.load("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2"),
257 specular_map: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"),
258 intensity: 2000.0,
259 ..default()
260 },
261 ));
262}
263
264fn add_basic_scene(commands: &mut Commands, asset_server: &AssetServer) {
265 commands.spawn(WorldAssetRoot(asset_server.load(
267 GltfAssetLabel::Scene(0).from_asset("models/TonemappingTest/TonemappingTest.gltf"),
268 )));
269
270 commands.spawn((
272 WorldAssetRoot(
273 asset_server
274 .load(GltfAssetLabel::Scene(0).from_asset("models/FlightHelmet/FlightHelmet.gltf")),
275 ),
276 Transform::from_xyz(0.5, 0.0, -0.5).with_rotation(Quat::from_rotation_y(-0.15 * PI)),
277 ));
278
279 commands.spawn((
281 DirectionalLight {
282 illuminance: 15000.0,
283 shadow_maps_enabled: true,
284 ..default()
285 },
286 Transform::from_rotation(Quat::from_euler(EulerRot::ZYX, 0.0, PI * -0.15, PI * -0.15)),
287 CascadeShadowConfigBuilder {
288 maximum_distance: 3.0,
289 first_cascade_far_bound: 0.9,
290 ..default()
291 }
292 .build(),
293 ));
294}
295
296fn handle_value_change_number_input(
298 value_change: On<ValueChange<f32>>,
299 mut commands: Commands,
300 setting_q: Query<&ColorGradingSetting, With<FeathersNumberInput>>,
301 mut color_grading: Single<&mut ColorGrading>,
302) {
303 if let Ok(setting) = setting_q.get(value_change.source) {
304 setting.set(&mut color_grading, value_change.value);
305
306 commands
307 .entity(value_change.source)
308 .insert(NumberInputValue::F32(value_change.value));
309 }
310}
311
312impl Display for GlobalColorGradingSetting {
313 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
314 let name = match *self {
315 GlobalColorGradingSetting::Exposure => "Exposure",
316 GlobalColorGradingSetting::Temperature => "Temperature",
317 GlobalColorGradingSetting::Tint => "Tint",
318 GlobalColorGradingSetting::Hue => "Hue",
319 };
320 f.write_str(name)
321 }
322}
323
324impl Display for SectionColorGradingName {
325 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
326 let name = match *self {
327 SectionColorGradingName::Highlights => "Highlights",
328 SectionColorGradingName::Midtones => "Midtones",
329 SectionColorGradingName::Shadows => "Shadows",
330 };
331 f.write_str(name)
332 }
333}
334
335impl Display for SectionColorGradingSetting {
336 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
337 let name = match *self {
338 SectionColorGradingSetting::Saturation => "Saturation",
339 SectionColorGradingSetting::Contrast => "Contrast",
340 SectionColorGradingSetting::Gamma => "Gamma",
341 SectionColorGradingSetting::Gain => "Gain",
342 SectionColorGradingSetting::Lift => "Lift",
343 };
344 f.write_str(name)
345 }
346}
347
348impl Display for ColorGradingSetting {
349 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
350 match self {
351 ColorGradingSetting::Global(option) => write!(f, "\"{option}\""),
352 ColorGradingSetting::Section(section, option) => {
353 write!(f, "\"{option}\" for \"{section}\"")
354 }
355 }
356 }
357}
358
359impl SectionColorGradingSetting {
360 fn get(&self, section: &ColorGradingSection) -> f32 {
362 match *self {
363 SectionColorGradingSetting::Saturation => section.saturation,
364 SectionColorGradingSetting::Contrast => section.contrast,
365 SectionColorGradingSetting::Gamma => section.gamma,
366 SectionColorGradingSetting::Gain => section.gain,
367 SectionColorGradingSetting::Lift => section.lift,
368 }
369 }
370
371 fn set(&self, section: &mut ColorGradingSection, value: f32) {
373 match *self {
374 SectionColorGradingSetting::Saturation => section.saturation = value,
375 SectionColorGradingSetting::Contrast => section.contrast = value,
376 SectionColorGradingSetting::Gamma => section.gamma = value,
377 SectionColorGradingSetting::Gain => section.gain = value,
378 SectionColorGradingSetting::Lift => section.lift = value,
379 }
380 }
381}
382
383impl GlobalColorGradingSetting {
384 fn get(&self, global: &ColorGradingGlobal) -> f32 {
387 match *self {
388 GlobalColorGradingSetting::Exposure => global.exposure,
389 GlobalColorGradingSetting::Temperature => global.temperature,
390 GlobalColorGradingSetting::Tint => global.tint,
391 GlobalColorGradingSetting::Hue => global.hue,
392 }
393 }
394
395 fn set(&self, global: &mut ColorGradingGlobal, value: f32) {
398 match *self {
399 GlobalColorGradingSetting::Exposure => global.exposure = value,
400 GlobalColorGradingSetting::Temperature => global.temperature = value,
401 GlobalColorGradingSetting::Tint => global.tint = value,
402 GlobalColorGradingSetting::Hue => global.hue = value,
403 }
404 }
405}
406
407impl ColorGradingSetting {
408 fn get(&self, color_grading: &ColorGrading) -> f32 {
410 match self {
411 ColorGradingSetting::Global(option) => option.get(&color_grading.global),
412 ColorGradingSetting::Section(SectionColorGradingName::Highlights, option) => {
413 option.get(&color_grading.highlights)
414 }
415 ColorGradingSetting::Section(SectionColorGradingName::Midtones, option) => {
416 option.get(&color_grading.midtones)
417 }
418 ColorGradingSetting::Section(SectionColorGradingName::Shadows, option) => {
419 option.get(&color_grading.shadows)
420 }
421 }
422 }
423
424 fn set(&self, color_grading: &mut ColorGrading, value: f32) {
426 match self {
427 ColorGradingSetting::Global(option) => {
428 option.set(&mut color_grading.global, value);
429 }
430 ColorGradingSetting::Section(SectionColorGradingName::Highlights, option) => {
431 option.set(&mut color_grading.highlights, value);
432 }
433 ColorGradingSetting::Section(SectionColorGradingName::Midtones, option) => {
434 option.set(&mut color_grading.midtones, value);
435 }
436 ColorGradingSetting::Section(SectionColorGradingName::Shadows, option) => {
437 option.set(&mut color_grading.shadows, value);
438 }
439 }
440 }
441}
442
443fn get_example_theme() -> ThemeProps {
445 let mut props = create_dark_theme();
446
447 for token in [SURFACE_PANE_BODY, SURFACE_PANE_HEADER] {
449 if let Some(color) = props.semantic_base.get_mut(&token) {
450 color.set_alpha(0.9);
451 }
452 }
453
454 props
455}
456
457fn add_help_text(commands: &mut Commands) {
459 commands.spawn_scene(bsn! {
460 Node {
461 position_type: PositionType::Absolute,
462 left: px(12),
463 top: px(12),
464 }
465 Children [
466 Text("Drag a setting's input value to change the scene.\n\
467 Click into an input field to change values via keyboard.\n\
468 Values must be between 0 and 10.")
469 ]
470 });
471}