1use bevy::{
4 asset::UnapprovedPathMode,
5 camera::Hdr,
6 core_pipeline::tonemapping::Tonemapping,
7 light::CascadeShadowConfigBuilder,
8 platform::collections::HashMap,
9 prelude::*,
10 reflect::TypePath,
11 render::{
12 render_resource::AsBindGroup,
13 view::{ColorGrading, ColorGradingGlobal, ColorGradingSection},
14 },
15 shader::ShaderRef,
16};
17use std::f32::consts::PI;
18
19const SHADER_ASSET_PATH: &str = "shaders/tonemapping_test_patterns.wgsl";
21
22fn main() {
23 App::new()
24 .add_plugins((
25 DefaultPlugins.set(AssetPlugin {
26 unapproved_path_mode: UnapprovedPathMode::Allow,
29 ..default()
30 }),
31 MaterialPlugin::<ColorGradientMaterial>::default(),
32 ))
33 .insert_resource(CameraTransform(
34 Transform::from_xyz(0.7, 0.7, 1.0).looking_at(Vec3::new(0.0, 0.3, 0.0), Vec3::Y),
35 ))
36 .init_resource::<PerMethodSettings>()
37 .insert_resource(CurrentScene(1))
38 .insert_resource(SelectedParameter { value: 0, max: 4 })
39 .add_systems(
40 Startup,
41 (
42 setup,
43 setup_basic_scene,
44 setup_color_gradient_scene,
45 setup_image_viewer_scene,
46 ),
47 )
48 .add_systems(
49 Update,
50 (
51 drag_drop_image,
52 resize_image,
53 toggle_scene,
54 toggle_tonemapping_method,
55 update_color_grading_settings,
56 update_ui,
57 ),
58 )
59 .run();
60}
61
62fn setup(
63 mut commands: Commands,
64 asset_server: Res<AssetServer>,
65 camera_transform: Res<CameraTransform>,
66) {
67 commands.spawn((
69 Camera3d::default(),
70 Hdr,
71 camera_transform.0,
72 DistanceFog {
73 color: Color::srgb_u8(43, 44, 47),
74 falloff: FogFalloff::Linear {
75 start: 1.0,
76 end: 8.0,
77 },
78 ..default()
79 },
80 EnvironmentMapLight {
81 diffuse_map: asset_server.load("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2"),
82 specular_map: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"),
83 intensity: 2000.0,
84 ..default()
85 },
86 ));
87
88 commands.spawn((
90 Text::default(),
91 Node {
92 position_type: PositionType::Absolute,
93 top: px(12),
94 left: px(12),
95 ..default()
96 },
97 ));
98}
99
100fn setup_basic_scene(mut commands: Commands, asset_server: Res<AssetServer>) {
101 commands.spawn((
103 WorldAssetRoot(asset_server.load(
104 GltfAssetLabel::Scene(0).from_asset("models/TonemappingTest/TonemappingTest.gltf"),
105 )),
106 SceneNumber(1),
107 ));
108
109 commands.spawn((
111 WorldAssetRoot(
112 asset_server
113 .load(GltfAssetLabel::Scene(0).from_asset("models/FlightHelmet/FlightHelmet.gltf")),
114 ),
115 Transform::from_xyz(0.5, 0.0, -0.5).with_rotation(Quat::from_rotation_y(-0.15 * PI)),
116 SceneNumber(1),
117 ));
118
119 commands.spawn((
121 DirectionalLight {
122 illuminance: 15_000.,
123 shadow_maps_enabled: true,
124 ..default()
125 },
126 Transform::from_rotation(Quat::from_euler(EulerRot::ZYX, 0.0, PI * -0.15, PI * -0.15)),
127 CascadeShadowConfigBuilder {
128 maximum_distance: 3.0,
129 first_cascade_far_bound: 0.9,
130 ..default()
131 }
132 .build(),
133 SceneNumber(1),
134 ));
135}
136
137fn setup_color_gradient_scene(
138 mut commands: Commands,
139 mut meshes: ResMut<Assets<Mesh>>,
140 mut materials: ResMut<Assets<ColorGradientMaterial>>,
141 camera_transform: Res<CameraTransform>,
142) {
143 let mut transform = camera_transform.0;
144 transform.translation += *transform.forward();
145
146 commands.spawn((
147 Mesh3d(meshes.add(Rectangle::new(0.7, 0.7))),
148 MeshMaterial3d(materials.add(ColorGradientMaterial {})),
149 transform,
150 Visibility::Hidden,
151 SceneNumber(2),
152 ));
153}
154
155fn setup_image_viewer_scene(
156 mut commands: Commands,
157 mut meshes: ResMut<Assets<Mesh>>,
158 mut materials: ResMut<Assets<StandardMaterial>>,
159 camera_transform: Res<CameraTransform>,
160) {
161 let mut transform = camera_transform.0;
162 transform.translation += *transform.forward();
163
164 commands.spawn((
166 Mesh3d(meshes.add(Rectangle::default())),
167 MeshMaterial3d(materials.add(StandardMaterial {
168 base_color_texture: None,
169 unlit: true,
170 ..default()
171 })),
172 transform,
173 Visibility::Hidden,
174 SceneNumber(3),
175 HDRViewer,
176 ));
177
178 commands.spawn((
179 Text::new("Drag and drop an HDR or EXR file"),
180 TextFont {
181 font_size: FontSize::Px(36.0),
182 ..default()
183 },
184 TextColor(Color::BLACK),
185 TextLayout::justify(Justify::Center),
186 Node {
187 align_self: AlignSelf::Center,
188 margin: UiRect::all(auto()),
189 ..default()
190 },
191 SceneNumber(3),
192 Visibility::Hidden,
193 ));
194}
195
196fn drag_drop_image(
199 image_mat: Query<&MeshMaterial3d<StandardMaterial>, With<HDRViewer>>,
200 text: Query<Entity, (With<Text>, With<SceneNumber>)>,
201 mut materials: ResMut<Assets<StandardMaterial>>,
202 mut drag_and_drop_reader: MessageReader<FileDragAndDrop>,
203 asset_server: Res<AssetServer>,
204 mut commands: Commands,
205) {
206 let Some(new_image) = drag_and_drop_reader.read().find_map(|e| match e {
207 FileDragAndDrop::DroppedFile { path_buf, .. } => {
208 Some(asset_server.load(path_buf.to_string_lossy().to_string()))
209 }
210 _ => None,
211 }) else {
212 return;
213 };
214
215 for mat_h in &image_mat {
216 if let Some(mut mat) = materials.get_mut(mat_h) {
217 mat.base_color_texture = Some(new_image.clone());
218
219 if let Ok(text_entity) = text.single() {
221 commands.entity(text_entity).despawn();
222 }
223 }
224 }
225}
226
227fn resize_image(
228 image_mesh: Query<(&MeshMaterial3d<StandardMaterial>, &Mesh3d), With<HDRViewer>>,
229 materials: Res<Assets<StandardMaterial>>,
230 mut meshes: ResMut<Assets<Mesh>>,
231 images: Res<Assets<Image>>,
232 mut image_event_reader: MessageReader<AssetEvent<Image>>,
233) {
234 for event in image_event_reader.read() {
235 let (AssetEvent::Added { id } | AssetEvent::Modified { id }) = event else {
236 continue;
237 };
238
239 for (mat_h, mesh_h) in &image_mesh {
240 let Some(mat) = materials.get(mat_h) else {
241 continue;
242 };
243
244 let Some(ref base_color_texture) = mat.base_color_texture else {
245 continue;
246 };
247
248 if *id != base_color_texture.id() {
249 continue;
250 };
251
252 let Some(image_changed) = images.get(*id) else {
253 continue;
254 };
255
256 let size = image_changed.size_f32().normalize_or_zero() * 1.4;
257 let quad = Mesh::from(Rectangle::from_size(size));
259 meshes.insert(mesh_h, quad).unwrap();
260 }
261 }
262}
263
264fn toggle_scene(
265 keys: Res<ButtonInput<KeyCode>>,
266 mut query: Query<(&mut Visibility, &SceneNumber)>,
267 mut current_scene: ResMut<CurrentScene>,
268) {
269 let mut pressed = None;
270 if keys.just_pressed(KeyCode::KeyQ) {
271 pressed = Some(1);
272 } else if keys.just_pressed(KeyCode::KeyW) {
273 pressed = Some(2);
274 } else if keys.just_pressed(KeyCode::KeyE) {
275 pressed = Some(3);
276 }
277
278 if let Some(pressed) = pressed {
279 current_scene.0 = pressed;
280
281 for (mut visibility, scene) in query.iter_mut() {
282 if scene.0 == pressed {
283 *visibility = Visibility::Visible;
284 } else {
285 *visibility = Visibility::Hidden;
286 }
287 }
288 }
289}
290
291fn toggle_tonemapping_method(
292 keys: Res<ButtonInput<KeyCode>>,
293 mut tonemapping: Single<&mut Tonemapping>,
294 mut color_grading: Single<&mut ColorGrading>,
295 per_method_settings: Res<PerMethodSettings>,
296) {
297 if keys.just_pressed(KeyCode::Digit1) {
298 **tonemapping = Tonemapping::None;
299 } else if keys.just_pressed(KeyCode::Digit2) {
300 **tonemapping = Tonemapping::Reinhard;
301 } else if keys.just_pressed(KeyCode::Digit3) {
302 **tonemapping = Tonemapping::ReinhardLuminance;
303 } else if keys.just_pressed(KeyCode::Digit4) {
304 **tonemapping = Tonemapping::AcesFitted;
305 } else if keys.just_pressed(KeyCode::Digit5) {
306 **tonemapping = Tonemapping::AgX;
307 } else if keys.just_pressed(KeyCode::Digit6) {
308 **tonemapping = Tonemapping::SomewhatBoringDisplayTransform;
309 } else if keys.just_pressed(KeyCode::Digit7) {
310 **tonemapping = Tonemapping::TonyMcMapface;
311 } else if keys.just_pressed(KeyCode::Digit8) {
312 **tonemapping = Tonemapping::BlenderFilmic;
313 } else if keys.just_pressed(KeyCode::Digit9) {
314 **tonemapping = Tonemapping::KhronosPbrNeutral;
315 }
316
317 **color_grading = (*per_method_settings
318 .settings
319 .get::<Tonemapping>(&tonemapping)
320 .as_ref()
321 .unwrap())
322 .clone();
323}
324
325#[derive(Resource)]
326struct SelectedParameter {
327 value: i32,
328 max: i32,
329}
330
331impl SelectedParameter {
332 fn next(&mut self) {
333 self.value = (self.value + 1).rem_euclid(self.max);
334 }
335 fn prev(&mut self) {
336 self.value = (self.value - 1).rem_euclid(self.max);
337 }
338}
339
340fn update_color_grading_settings(
341 keys: Res<ButtonInput<KeyCode>>,
342 time: Res<Time>,
343 mut per_method_settings: ResMut<PerMethodSettings>,
344 tonemapping: Single<&Tonemapping>,
345 current_scene: Res<CurrentScene>,
346 mut selected_parameter: ResMut<SelectedParameter>,
347) {
348 let color_grading = per_method_settings.settings.get_mut(*tonemapping).unwrap();
349 let mut dt = time.delta_secs() * 0.25;
350 if keys.pressed(KeyCode::ArrowLeft) {
351 dt = -dt;
352 }
353
354 if keys.just_pressed(KeyCode::ArrowDown) {
355 selected_parameter.next();
356 }
357 if keys.just_pressed(KeyCode::ArrowUp) {
358 selected_parameter.prev();
359 }
360 if keys.pressed(KeyCode::ArrowLeft) || keys.pressed(KeyCode::ArrowRight) {
361 match selected_parameter.value {
362 0 => {
363 color_grading.global.exposure += dt;
364 }
365 1 => {
366 color_grading
367 .all_sections_mut()
368 .for_each(|section| section.gamma += dt);
369 }
370 2 => {
371 color_grading
372 .all_sections_mut()
373 .for_each(|section| section.saturation += dt);
374 }
375 3 => {
376 color_grading.global.post_saturation += dt;
377 }
378 _ => {}
379 }
380 }
381
382 if keys.just_pressed(KeyCode::Space) {
383 for (_, grading) in per_method_settings.settings.iter_mut() {
384 *grading = ColorGrading::default();
385 }
386 }
387
388 if keys.just_pressed(KeyCode::Enter) && current_scene.0 == 1 {
389 for (mapper, grading) in per_method_settings.settings.iter_mut() {
390 *grading = PerMethodSettings::basic_scene_recommendation(*mapper);
391 }
392 }
393}
394
395fn update_ui(
396 mut text_query: Single<&mut Text, Without<SceneNumber>>,
397 settings: Single<(&Tonemapping, &ColorGrading)>,
398 current_scene: Res<CurrentScene>,
399 selected_parameter: Res<SelectedParameter>,
400 mut hide_ui: Local<bool>,
401 keys: Res<ButtonInput<KeyCode>>,
402) {
403 if keys.just_pressed(KeyCode::KeyH) {
404 *hide_ui = !*hide_ui;
405 }
406
407 if *hide_ui {
408 if !text_query.is_empty() {
409 text_query.clear();
412 }
413 return;
414 }
415
416 let (tonemapping, color_grading) = *settings;
417 let tonemapping = *tonemapping;
418
419 let mut text = String::with_capacity(text_query.len());
420
421 let scn = current_scene.0;
422 text.push_str("(H) Hide UI\n\n");
423 text.push_str("Test Scene: \n");
424 text.push_str(&format!(
425 "(Q) {} Basic Scene\n",
426 if scn == 1 { ">" } else { "" }
427 ));
428 text.push_str(&format!(
429 "(W) {} Color Sweep\n",
430 if scn == 2 { ">" } else { "" }
431 ));
432 text.push_str(&format!(
433 "(E) {} Image Viewer\n",
434 if scn == 3 { ">" } else { "" }
435 ));
436
437 text.push_str("\n\nTonemapping Method:\n");
438 text.push_str(&format!(
439 "(1) {} Disabled\n",
440 if tonemapping == Tonemapping::None {
441 ">"
442 } else {
443 ""
444 }
445 ));
446 text.push_str(&format!(
447 "(2) {} Reinhard\n",
448 if tonemapping == Tonemapping::Reinhard {
449 "> "
450 } else {
451 ""
452 }
453 ));
454 text.push_str(&format!(
455 "(3) {} Reinhard Luminance\n",
456 if tonemapping == Tonemapping::ReinhardLuminance {
457 ">"
458 } else {
459 ""
460 }
461 ));
462 text.push_str(&format!(
463 "(4) {} ACES Fitted\n",
464 if tonemapping == Tonemapping::AcesFitted {
465 ">"
466 } else {
467 ""
468 }
469 ));
470 text.push_str(&format!(
471 "(5) {} AgX\n",
472 if tonemapping == Tonemapping::AgX {
473 ">"
474 } else {
475 ""
476 }
477 ));
478 text.push_str(&format!(
479 "(6) {} SomewhatBoringDisplayTransform\n",
480 if tonemapping == Tonemapping::SomewhatBoringDisplayTransform {
481 ">"
482 } else {
483 ""
484 }
485 ));
486 text.push_str(&format!(
487 "(7) {} TonyMcMapface\n",
488 if tonemapping == Tonemapping::TonyMcMapface {
489 ">"
490 } else {
491 ""
492 }
493 ));
494 text.push_str(&format!(
495 "(8) {} Blender Filmic\n",
496 if tonemapping == Tonemapping::BlenderFilmic {
497 ">"
498 } else {
499 ""
500 }
501 ));
502 text.push_str(&format!(
503 "(9) {} Khronos PBR Neutral\n",
504 if tonemapping == Tonemapping::KhronosPbrNeutral {
505 ">"
506 } else {
507 ""
508 }
509 ));
510
511 text.push_str("\n\nColor Grading:\n");
512 text.push_str("(arrow keys)\n");
513 if selected_parameter.value == 0 {
514 text.push_str("> ");
515 }
516 text.push_str(&format!("Exposure: {:.2}\n", color_grading.global.exposure));
517 if selected_parameter.value == 1 {
518 text.push_str("> ");
519 }
520 text.push_str(&format!("Gamma: {:.2}\n", color_grading.shadows.gamma));
521 if selected_parameter.value == 2 {
522 text.push_str("> ");
523 }
524 text.push_str(&format!(
525 "PreSaturation: {:.2}\n",
526 color_grading.shadows.saturation
527 ));
528 if selected_parameter.value == 3 {
529 text.push_str("> ");
530 }
531 text.push_str(&format!(
532 "PostSaturation: {:.2}\n",
533 color_grading.global.post_saturation
534 ));
535 text.push_str("(Space) Reset all to default\n");
536
537 if current_scene.0 == 1 {
538 text.push_str("(Enter) Reset all to scene recommendation\n");
539 }
540
541 if text != text_query.as_str() {
542 text_query.0 = text;
545 }
546}
547
548#[derive(Resource)]
551struct PerMethodSettings {
552 settings: HashMap<Tonemapping, ColorGrading>,
553}
554
555impl PerMethodSettings {
556 fn basic_scene_recommendation(method: Tonemapping) -> ColorGrading {
557 match method {
558 Tonemapping::Reinhard | Tonemapping::ReinhardLuminance => ColorGrading {
559 global: ColorGradingGlobal {
560 exposure: 0.5,
561 ..default()
562 },
563 ..default()
564 },
565 Tonemapping::AcesFitted => ColorGrading {
566 global: ColorGradingGlobal {
567 exposure: 0.35,
568 ..default()
569 },
570 ..default()
571 },
572 Tonemapping::AgX => ColorGrading::with_identical_sections(
573 ColorGradingGlobal {
574 exposure: -0.2,
575 post_saturation: 1.1,
576 ..default()
577 },
578 ColorGradingSection {
579 saturation: 1.1,
580 ..default()
581 },
582 ),
583 _ => ColorGrading::default(),
584 }
585 }
586}
587
588impl Default for PerMethodSettings {
589 fn default() -> Self {
590 let mut settings = <HashMap<_, _>>::default();
591
592 for method in [
593 Tonemapping::None,
594 Tonemapping::Reinhard,
595 Tonemapping::ReinhardLuminance,
596 Tonemapping::AcesFitted,
597 Tonemapping::AgX,
598 Tonemapping::SomewhatBoringDisplayTransform,
599 Tonemapping::TonyMcMapface,
600 Tonemapping::BlenderFilmic,
601 Tonemapping::KhronosPbrNeutral,
602 ] {
603 settings.insert(
604 method,
605 PerMethodSettings::basic_scene_recommendation(method),
606 );
607 }
608
609 Self { settings }
610 }
611}
612
613impl Material for ColorGradientMaterial {
614 fn fragment_shader() -> ShaderRef {
615 SHADER_ASSET_PATH.into()
616 }
617}
618
619#[derive(Asset, TypePath, AsBindGroup, Debug, Clone)]
620struct ColorGradientMaterial {}
621
622#[derive(Resource)]
623struct CameraTransform(Transform);
624
625#[derive(Resource)]
626struct CurrentScene(u32);
627
628#[derive(Component)]
629struct SceneNumber(u32);
630
631#[derive(Component)]
632struct HDRViewer;