diff --git a/_release-content/migration-guides/tonemapping_none_passthrough.md b/_release-content/migration-guides/tonemapping_none_passthrough.md index 0c261e6e5d477..d065c2d303a40 100644 --- a/_release-content/migration-guides/tonemapping_none_passthrough.md +++ b/_release-content/migration-guides/tonemapping_none_passthrough.md @@ -1,14 +1,25 @@ --- title: "`Tonemapping::None` is now a full passthrough" -pull_requests: [25499] +pull_requests: [25499, 25685] --- -`Tonemapping::None` now leaves the image completely untouched. `ColorGrading` and -`DebandDither` stop applying. +`Tonemapping::None` is now a full passthrough. `ColorGrading` and `DebandDither` no longer +apply under it, and negative color channels are no longer clamped to zero. `Camera3d` +enables `DebandDither` by default, so a `Camera3d` without `Hdr` that used +`Tonemapping::None` renders differently. If you used `Tonemapping::None` to turn off the +tone curve, use the new `Tonemapping::Linear` instead. It applies no tone curve and keeps +grading, dither, and the clamp. -If you use `Tonemapping::None` with `ColorGrading` or `DebandDither`, switch to the -new `Tonemapping::Linear`. It applies no tone curve but keeps grading and dither -working exactly as they do under the named operators. +```rust +// 0.19 +commands.spawn((Camera3d::default(), Tonemapping::None)); -`Tonemapping::None` also stops clamping negative color channels to zero. If your -scene relies on that clamp, `Tonemapping::Linear` keeps it. +// 0.20 +commands.spawn((Camera3d::default(), Tonemapping::Linear)); +``` + +`Camera2d` now defaults to `Tonemapping::Linear`. No change is needed for 2D cameras. A +`Camera2d` with `Hdr` now runs the tonemapping pass. + +Bevy logs a warning for a camera that combines `Tonemapping::None` with `DebandDither::Enabled` +or a non-default `ColorGrading`. diff --git a/crates/bevy_core_pipeline/src/core_2d/mod.rs b/crates/bevy_core_pipeline/src/core_2d/mod.rs index fa16c298d5b63..80641875b5d10 100644 --- a/crates/bevy_core_pipeline/src/core_2d/mod.rs +++ b/crates/bevy_core_pipeline/src/core_2d/mod.rs @@ -53,7 +53,7 @@ impl Plugin for Core2dPlugin { .register_required_components_with::(|| { CameraRenderGraph::new(Core2d) }) - .register_required_components_with::(|| Tonemapping::None) + .register_required_components_with::(|| Tonemapping::Linear) .add_plugins(ExtractComponentPlugin::::default()); let Some(render_app) = app.get_sub_app_mut(RenderApp) else { diff --git a/crates/bevy_core_pipeline/src/tonemapping/mod.rs b/crates/bevy_core_pipeline/src/tonemapping/mod.rs index c375e353dce60..c34ab6876dc2b 100644 --- a/crates/bevy_core_pipeline/src/tonemapping/mod.rs +++ b/crates/bevy_core_pipeline/src/tonemapping/mod.rs @@ -2,10 +2,12 @@ use bevy_app::prelude::*; use bevy_asset::{ embedded_asset, load_embedded_asset, AssetServer, Assets, Handle, RenderAssetUsages, }; +use bevy_camera::Camera; use bevy_ecs::prelude::*; use bevy_image::{CompressedImageFormats, Image, ImageSampler, ImageType}; #[cfg(not(feature = "tonemapping_luts"))] use bevy_log::error; +use bevy_log::warn; use bevy_render::{ extract_component::ExtractComponentPlugin, extract_resource::{ExtractResource, ExtractResourcePlugin}, @@ -16,7 +18,7 @@ use bevy_render::{ }, renderer::RenderDevice, texture::{FallbackImage, GpuImage}, - view::{ExtractedView, ViewTarget, ViewUniform}, + view::{ColorGrading, ExtractedView, ViewTarget, ViewUniform}, GpuResourceAppExt, Render, RenderApp, RenderStartup, RenderSystems, }; use bevy_shader::{load_shader_library, Shader, ShaderDefVal}; @@ -88,6 +90,8 @@ impl Plugin for TonemappingPlugin { ExtractComponentPlugin::::default(), )); + app.add_systems(PostUpdate, check_tonemapping_none); + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { return; }; @@ -101,6 +105,46 @@ impl Plugin for TonemappingPlugin { } } +/// Warns when a camera has [`Tonemapping::None`] together with +/// [`DebandDither::Enabled`] or a non-default [`ColorGrading`], since neither +/// has any effect without tonemapping. +pub fn check_tonemapping_none( + cameras: Query< + ( + NameOrEntity, + &Tonemapping, + Option<&DebandDither>, + Option<&ColorGrading>, + ), + ( + With, + Or<( + Changed, + Changed, + Changed, + )>, + ), + >, +) { + for (camera, tonemapping, dither, color_grading) in &cameras { + if tonemapping.is_enabled() { + continue; + } + if dither == Some(&DebandDither::Enabled) { + warn!( + "Camera {camera} has `Tonemapping::None` with `DebandDither::Enabled`, so dithering has no effect. \ + Use `Tonemapping::Linear` to keep dithering, or set `DebandDither::Disabled`." + ); + } + if color_grading.is_some_and(|grading| *grading != ColorGrading::default()) { + warn!( + "Camera {camera} has `Tonemapping::None` with a non-default `ColorGrading`, so color grading has no effect. \ + Use `Tonemapping::Linear` to keep color grading, or set `ColorGrading::default()`." + ); + } + } +} + #[derive(Resource)] pub struct TonemappingPipeline { texture_bind_group: BindGroupLayoutDescriptor, diff --git a/crates/bevy_render/src/view/mod.rs b/crates/bevy_render/src/view/mod.rs index 8169cdd415f45..0af4c26b7c944 100644 --- a/crates/bevy_render/src/view/mod.rs +++ b/crates/bevy_render/src/view/mod.rs @@ -485,8 +485,8 @@ impl ExtractedView { /// Color grading is applied just before tonemapping for a given [`Camera`] /// entity, with the sole exception of the `post_saturation` value in /// [`ColorGradingGlobal`], which is applied after tonemapping. -#[derive(Component, Reflect, Debug, Default, Clone)] -#[reflect(Component, Default, Debug, Clone)] +#[derive(Component, Reflect, Debug, Default, Clone, PartialEq)] +#[reflect(Component, Default, Debug, Clone, PartialEq)] pub struct ColorGrading { /// Filmic color grading values applied to the image as a whole (as opposed /// to individual sections, like shadows and highlights). @@ -514,8 +514,8 @@ pub struct ColorGrading { /// Filmic color grading values applied to the image as a whole (as opposed to /// individual sections, like shadows and highlights). -#[derive(Clone, Debug, Reflect)] -#[reflect(Default, Clone)] +#[derive(Clone, Debug, Reflect, PartialEq)] +#[reflect(Default, Clone, PartialEq)] pub struct ColorGradingGlobal { /// Exposure value (EV) offset, measured in stops. pub exposure: f32, diff --git a/examples/2d/bloom_2d.rs b/examples/2d/bloom_2d.rs index e332966fddedb..d7087840619d4 100644 --- a/examples/2d/bloom_2d.rs +++ b/examples/2d/bloom_2d.rs @@ -202,7 +202,6 @@ fn update_bloom_settings( /// Get the next Tonemapping algorithm fn next_tonemap(tonemapping: &Tonemapping) -> Tonemapping { match tonemapping { - Tonemapping::None => Tonemapping::Linear, Tonemapping::Linear => Tonemapping::AcesFitted, Tonemapping::AcesFitted => Tonemapping::AgX, Tonemapping::AgX => Tonemapping::BlenderFilmic, @@ -211,6 +210,6 @@ fn next_tonemap(tonemapping: &Tonemapping) -> Tonemapping { Tonemapping::ReinhardLuminance => Tonemapping::SomewhatBoringDisplayTransform, Tonemapping::SomewhatBoringDisplayTransform => Tonemapping::TonyMcMapface, Tonemapping::TonyMcMapface => Tonemapping::KhronosPbrNeutral, - Tonemapping::KhronosPbrNeutral => Tonemapping::None, + Tonemapping::None | Tonemapping::KhronosPbrNeutral => Tonemapping::Linear, } } diff --git a/examples/3d/tonemapping.rs b/examples/3d/tonemapping.rs index 83ee79343ddc6..f6541409a932a 100644 --- a/examples/3d/tonemapping.rs +++ b/examples/3d/tonemapping.rs @@ -295,7 +295,7 @@ fn toggle_tonemapping_method( per_method_settings: Res, ) { if keys.just_pressed(KeyCode::Digit1) { - **tonemapping = Tonemapping::None; + **tonemapping = Tonemapping::Linear; } else if keys.just_pressed(KeyCode::Digit2) { **tonemapping = Tonemapping::Reinhard; } else if keys.just_pressed(KeyCode::Digit3) { @@ -436,8 +436,8 @@ fn update_ui( text.push_str("\n\nTonemapping Method:\n"); text.push_str(&format!( - "(1) {} Disabled\n", - if tonemapping == Tonemapping::None { + "(1) {} Linear\n", + if tonemapping == Tonemapping::Linear { ">" } else { "" @@ -590,7 +590,7 @@ impl Default for PerMethodSettings { let mut settings = >::default(); for method in [ - Tonemapping::None, + Tonemapping::Linear, Tonemapping::Reinhard, Tonemapping::ReinhardLuminance, Tonemapping::AcesFitted, diff --git a/examples/app/headless_renderer.rs b/examples/app/headless_renderer.rs index c001a86a37b4b..88cd2013a3520 100644 --- a/examples/app/headless_renderer.rs +++ b/examples/app/headless_renderer.rs @@ -194,7 +194,7 @@ fn setup( commands.spawn(( Camera3d::default(), render_target, - Tonemapping::None, + Tonemapping::Linear, Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y), )); } diff --git a/examples/testbed/3d.rs b/examples/testbed/3d.rs index 4770c102fb660..060d985237f9b 100644 --- a/examples/testbed/3d.rs +++ b/examples/testbed/3d.rs @@ -512,7 +512,7 @@ mod white_furnace_solid_color_light { use bevy::{ asset::RenderAssetUsages, camera::{Hdr, ScalingMode}, - core_pipeline::tonemapping::Tonemapping, + core_pipeline::tonemapping::{DebandDither, Tonemapping}, light::Skybox, prelude::*, render::render_resource::{ @@ -602,6 +602,7 @@ mod white_furnace_solid_color_light { Camera3d::default(), Hdr, Tonemapping::None, + DebandDither::Disabled, Transform::from_xyz(0.0, 0.0, 8.0).looking_at(Vec3::default(), Vec3::Y), Projection::from(OrthographicProjection { scale: 0.01, @@ -624,7 +625,7 @@ mod white_furnace_environment_map_light { use bevy::{ asset::RenderAssetUsages, camera::{Hdr, ScalingMode}, - core_pipeline::tonemapping::Tonemapping, + core_pipeline::tonemapping::{DebandDither, Tonemapping}, light::Skybox, prelude::*, render::render_resource::{ @@ -717,6 +718,7 @@ mod white_furnace_environment_map_light { Camera3d::default(), Hdr, Tonemapping::None, + DebandDither::Disabled, Transform::from_xyz(0.0, 0.0, 8.0).looking_at(Vec3::default(), Vec3::Y), Projection::from(OrthographicProjection { scale: 0.01,