Skip to main content

tonemapping/
tonemapping.rs

1//! This examples compares Tonemapping options
2
3use 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
19/// This example uses a shader source file from the assets subdirectory
20const SHADER_ASSET_PATH: &str = "shaders/tonemapping_test_patterns.wgsl";
21
22fn main() {
23    App::new()
24        .add_plugins((
25            DefaultPlugins.set(AssetPlugin {
26                // We enable loading assets from arbitrary filesystem paths as this example allows
27                // drag and dropping a local image for color grading
28                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    // camera
68    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    // ui
89    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    // Main scene
102    commands.spawn((
103        WorldAssetRoot(asset_server.load(
104            GltfAssetLabel::Scene(0).from_asset("models/TonemappingTest/TonemappingTest.gltf"),
105        )),
106        SceneNumber(1),
107    ));
108
109    // Flight Helmet
110    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    // light
120    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    // exr/hdr viewer (exr requires enabling bevy feature)
165    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
196// ----------------------------------------------------------------------------
197
198fn 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            // Despawn the image viewer instructions
220            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            // Resize Mesh
258            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            // single_mut() always triggers change detection,
410            // so only access if text actually needs changing
411            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        // single_mut() always triggers change detection,
543        // so only access if text actually changed
544        text_query.0 = text;
545    }
546}
547
548// ----------------------------------------------------------------------------
549
550#[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;