tonemapping/
tonemapping.rs

1//! This examples compares Tonemapping options
2
3use 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
18/// This example uses a shader source file from the assets subdirectory
19const SHADER_ASSET_PATH: &str = "shaders/tonemapping_test_patterns.wgsl";
20
21fn main() {
22    App::new()
23        .add_plugins((
24            DefaultPlugins.set(AssetPlugin {
25                // We enable loading assets from arbitrary filesystem paths as this example allows
26                // drag and dropping a local image for color grading
27                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    // camera
67    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    // ui
88    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    // Main scene
101    commands.spawn((
102        SceneRoot(asset_server.load(
103            GltfAssetLabel::Scene(0).from_asset("models/TonemappingTest/TonemappingTest.gltf"),
104        )),
105        SceneNumber(1),
106    ));
107
108    // Flight Helmet
109    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    // light
119    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    // exr/hdr viewer (exr requires enabling bevy feature)
164    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
195// ----------------------------------------------------------------------------
196
197fn 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            // Despawn the image viewer instructions
219            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            // Resize Mesh
257            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            // single_mut() always triggers change detection,
407            // so only access if text actually needs changing
408            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        // single_mut() always triggers change detection,
532        // so only access if text actually changed
533        text_query.0 = text;
534    }
535}
536
537// ----------------------------------------------------------------------------
538
539#[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;