From fe7f476a92c14bd244432dc0da7e2a266105386b Mon Sep 17 00:00:00 2001 From: Stuart Parmenter Date: Fri, 4 Sep 2026 10:12:06 -0700 Subject: [PATCH 1/6] Name the affected cameras in the Tonemapping::None migration guide Camera3d enables DebandDither by default and Camera2d defaulted to Tonemapping::None, so a camera that used None in 0.19 loses dither or color grading without any change of its own. Say which cameras are affected and that Tonemapping::Linear keeps the 0.19 result. --- .../tonemapping_none_passthrough.md | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/_release-content/migration-guides/tonemapping_none_passthrough.md b/_release-content/migration-guides/tonemapping_none_passthrough.md index 0c261e6e5d477..80cbfd4b8c4e7 100644 --- a/_release-content/migration-guides/tonemapping_none_passthrough.md +++ b/_release-content/migration-guides/tonemapping_none_passthrough.md @@ -3,12 +3,20 @@ title: "`Tonemapping::None` is now a full passthrough" pull_requests: [25499] --- -`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` defaults to `Tonemapping::None`, so a `Camera2d` with `DebandDither::Enabled` or +`ColorGrading` now renders without them. Add `Tonemapping::Linear` to keep the 0.19 result. From 3fecfab4ec484c65c2803ef76185fe42d955b296 Mon Sep 17 00:00:00 2001 From: Stuart Parmenter Date: Fri, 4 Sep 2026 10:15:58 -0700 Subject: [PATCH 2/6] Warn when Tonemapping::None makes dither or color grading a no-op Add a PostUpdate system that logs a warning for a camera that has Tonemapping::None together with DebandDither::Enabled or a non-default ColorGrading. It runs on change detection, so it fires on spawn and on each change to those components. ColorGrading and ColorGradingGlobal derive PartialEq for the default comparison. Update the examples that combine None with dither or grading so they do not log the warning. The testbed white furnace cameras add DebandDither::Disabled, since they want exact output. The headless renderer and the tonemapping example use Linear in place of None. The bloom_2d cycle wraps to Linear. Mention the warning in the migration guide. --- .../tonemapping_none_passthrough.md | 3 ++ .../bevy_core_pipeline/src/tonemapping/mod.rs | 46 ++++++++++++++++++- crates/bevy_render/src/view/mod.rs | 8 ++-- examples/2d/bloom_2d.rs | 2 +- examples/3d/tonemapping.rs | 8 ++-- examples/app/headless_renderer.rs | 2 +- examples/testbed/3d.rs | 6 ++- 7 files changed, 62 insertions(+), 13 deletions(-) diff --git a/_release-content/migration-guides/tonemapping_none_passthrough.md b/_release-content/migration-guides/tonemapping_none_passthrough.md index 80cbfd4b8c4e7..592cd625bb50b 100644 --- a/_release-content/migration-guides/tonemapping_none_passthrough.md +++ b/_release-content/migration-guides/tonemapping_none_passthrough.md @@ -20,3 +20,6 @@ commands.spawn((Camera3d::default(), Tonemapping::Linear)); `Camera2d` defaults to `Tonemapping::None`, so a `Camera2d` with `DebandDither::Enabled` or `ColorGrading` now renders without them. Add `Tonemapping::Linear` to keep the 0.19 result. + +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/tonemapping/mod.rs b/crates/bevy_core_pipeline/src/tonemapping/mod.rs index c375e353dce60..4f13b78d5e0b6 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< + ( + Entity, + &Tonemapping, + Option<&DebandDither>, + Option<&ColorGrading>, + ), + ( + With, + Or<( + Changed, + Changed, + Changed, + )>, + ), + >, +) { + for (entity, tonemapping, dither, color_grading) in &cameras { + if tonemapping.is_enabled() { + continue; + } + if dither == Some(&DebandDither::Enabled) { + warn!( + "Entity {entity} 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!( + "Entity {entity} 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..0b91ff294de2b 100644 --- a/examples/2d/bloom_2d.rs +++ b/examples/2d/bloom_2d.rs @@ -211,6 +211,6 @@ fn next_tonemap(tonemapping: &Tonemapping) -> Tonemapping { Tonemapping::ReinhardLuminance => Tonemapping::SomewhatBoringDisplayTransform, Tonemapping::SomewhatBoringDisplayTransform => Tonemapping::TonyMcMapface, Tonemapping::TonyMcMapface => Tonemapping::KhronosPbrNeutral, - Tonemapping::KhronosPbrNeutral => 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, From 4ddb86d54dee02b52a53d2e24e287ac7335659b1 Mon Sep 17 00:00:00 2001 From: Stuart Parmenter Date: Fri, 4 Sep 2026 10:17:48 -0700 Subject: [PATCH 3/6] Default Camera2d to Tonemapping::Linear Camera2d defaulted to Tonemapping::None, which is a full passthrough. A 2D camera with DebandDither::Enabled or a ColorGrading component lost both unless the user also set a tonemapping method. Linear applies no tone curve but keeps dither, grading, and the clamp, so 2D cameras keep those settings by default. None stays the explicit passthrough on both camera types. A Camera2d with Hdr now runs the tonemapping pass every frame. The pass applies the identity curve, the clamp, and any dither or grading set on the camera. Set Tonemapping::None to skip the pass. Update the migration guide to say so. --- .../migration-guides/tonemapping_none_passthrough.md | 4 ++-- crates/bevy_core_pipeline/src/core_2d/mod.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/_release-content/migration-guides/tonemapping_none_passthrough.md b/_release-content/migration-guides/tonemapping_none_passthrough.md index 592cd625bb50b..7eb3297d15903 100644 --- a/_release-content/migration-guides/tonemapping_none_passthrough.md +++ b/_release-content/migration-guides/tonemapping_none_passthrough.md @@ -18,8 +18,8 @@ commands.spawn((Camera3d::default(), Tonemapping::None)); commands.spawn((Camera3d::default(), Tonemapping::Linear)); ``` -`Camera2d` defaults to `Tonemapping::None`, so a `Camera2d` with `DebandDither::Enabled` or -`ColorGrading` now renders without them. Add `Tonemapping::Linear` to keep the 0.19 result. +`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 { From b82fb57bd264105c23f83c99582282ece1e06caf Mon Sep 17 00:00:00 2001 From: Stuart Parmenter Date: Fri, 4 Sep 2026 11:10:11 -0700 Subject: [PATCH 4/6] Add the follow-up PR number to the Tonemapping::None migration guide --- .../migration-guides/tonemapping_none_passthrough.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_release-content/migration-guides/tonemapping_none_passthrough.md b/_release-content/migration-guides/tonemapping_none_passthrough.md index 7eb3297d15903..d065c2d303a40 100644 --- a/_release-content/migration-guides/tonemapping_none_passthrough.md +++ b/_release-content/migration-guides/tonemapping_none_passthrough.md @@ -1,6 +1,6 @@ --- title: "`Tonemapping::None` is now a full passthrough" -pull_requests: [25499] +pull_requests: [25499, 25685] --- `Tonemapping::None` is now a full passthrough. `ColorGrading` and `DebandDither` no longer From 06c92d299d3156212ee9db75cd00569fb8becee2 Mon Sep 17 00:00:00 2001 From: Stuart Parmenter Date: Fri, 4 Sep 2026 11:24:45 -0700 Subject: [PATCH 5/6] fix ci --- examples/2d/bloom_2d.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/2d/bloom_2d.rs b/examples/2d/bloom_2d.rs index 0b91ff294de2b..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::Linear, + Tonemapping::None | Tonemapping::KhronosPbrNeutral => Tonemapping::Linear, } } From db15c149851ac313551b1449bd4b7902d34a8e01 Mon Sep 17 00:00:00 2001 From: Stuart Parmenter Date: Fri, 4 Sep 2026 16:25:59 -0700 Subject: [PATCH 6/6] Use NameOrEntity in the tonemapping warning --- crates/bevy_core_pipeline/src/tonemapping/mod.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/bevy_core_pipeline/src/tonemapping/mod.rs b/crates/bevy_core_pipeline/src/tonemapping/mod.rs index 4f13b78d5e0b6..c34ab6876dc2b 100644 --- a/crates/bevy_core_pipeline/src/tonemapping/mod.rs +++ b/crates/bevy_core_pipeline/src/tonemapping/mod.rs @@ -111,7 +111,7 @@ impl Plugin for TonemappingPlugin { pub fn check_tonemapping_none( cameras: Query< ( - Entity, + NameOrEntity, &Tonemapping, Option<&DebandDither>, Option<&ColorGrading>, @@ -126,19 +126,19 @@ pub fn check_tonemapping_none( ), >, ) { - for (entity, tonemapping, dither, color_grading) in &cameras { + for (camera, tonemapping, dither, color_grading) in &cameras { if tonemapping.is_enabled() { continue; } if dither == Some(&DebandDither::Enabled) { warn!( - "Entity {entity} has `Tonemapping::None` with `DebandDither::Enabled`, so dithering has no effect. \ + "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!( - "Entity {entity} has `Tonemapping::None` with a non-default `ColorGrading`, so color grading has no effect. \ + "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()`." ); }