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