diff --git a/crates/bevy_camera/src/components.rs b/crates/bevy_camera/src/components.rs index 31da17f988071..e5c3b7204fdde 100644 --- a/crates/bevy_camera/src/components.rs +++ b/crates/bevy_camera/src/components.rs @@ -95,8 +95,8 @@ pub enum CompositingSpace { /// Gamma-encoded blending. Matches most image editors. Uses default sRGB target. #[default] Srgb, - /// Linear light blending. Physically correct. Requires [`Hdr`]. + /// Linear light blending. Physically correct. Linear, - /// Perceptually uniform blending. Often smoother gradients. Requires [`Hdr`]. + /// Perceptually uniform blending. Often smoother gradients. Requires [`Hdr`] because it can be outside the [0, 1]. Oklab, } diff --git a/crates/bevy_color/src/srgba.rs b/crates/bevy_color/src/srgba.rs index 1c8af76631dc0..0175c92239993 100644 --- a/crates/bevy_color/src/srgba.rs +++ b/crates/bevy_color/src/srgba.rs @@ -415,6 +415,18 @@ impl From for Xyza { } } +#[cfg(feature = "wgpu-types")] +impl From for wgpu_types::Color { + fn from(color: Srgba) -> Self { + wgpu_types::Color { + r: color.red as f64, + g: color.green as f64, + b: color.blue as f64, + a: color.alpha as f64, + } + } +} + /// Error returned if a hex string could not be parsed as a color. #[derive(Debug, Error, PartialEq, Eq)] pub enum HexColorError { diff --git a/crates/bevy_image/src/image.rs b/crates/bevy_image/src/image.rs index a16575b0b064a..310782153af67 100644 --- a/crates/bevy_image/src/image.rs +++ b/crates/bevy_image/src/image.rs @@ -42,16 +42,16 @@ impl BevyDefault for TextureFormat { } } -/// Trait used to provide texture srgb view formats with static lifetime for `TextureDescriptor.view_formats`. +/// Trait used to provide texture's srgb formats with static lifetime for `TextureDescriptor.view_formats`. pub trait TextureSrgbViewFormats { /// Returns the srgb view formats for a type. fn srgb_view_formats(&self) -> &'static [TextureFormat]; } impl TextureSrgbViewFormats for TextureFormat { - /// Returns the srgb view formats if the format has an srgb variant, otherwise returns an empty slice. + /// Returns the srgb formats if the format has an srgb variant, otherwise returns an empty slice. /// - /// The return result covers all the results of [`TextureFormat::add_srgb_suffix`](wgpu_types::TextureFormat::add_srgb_suffix). + /// Covers all the possible results of [`TextureFormat::add_srgb_suffix`](wgpu_types::TextureFormat::add_srgb_suffix). fn srgb_view_formats(&self) -> &'static [TextureFormat] { match self { TextureFormat::Rgba8Unorm => &[TextureFormat::Rgba8UnormSrgb], diff --git a/crates/bevy_render/src/camera.rs b/crates/bevy_render/src/camera.rs index 01605d985d754..0bd2699269cb6 100644 --- a/crates/bevy_render/src/camera.rs +++ b/crates/bevy_render/src/camera.rs @@ -610,13 +610,19 @@ pub fn extract_cameras( .map(|format| normalize_bgra8(target, format)) }) .unwrap_or(TextureFormat::Rgba8UnormSrgb); + let target_format = if hdr { TextureFormat::Rgba16Float - } else if compositing_space.is_some_and(|s| *s == CompositingSpace::Srgb) { - TextureFormat::Rgba8Unorm } else { output_texture_format - }; + } + // Regardless of whether the target format is srgb, + // we use the non-srgb main texture, otherwise it cannot support storage binding. + // + // It doesn't matter what the format we use for the main texture + // as it's intermediate and independent from render target. + .remove_srgb_suffix(); + main_pass_formats.insert(render_entity, target_format); let mut commands = commands.entity(render_entity); diff --git a/crates/bevy_render/src/texture/texture_attachment.rs b/crates/bevy_render/src/texture/texture_attachment.rs index c2d3ed8f6b952..e6f53112ca330 100644 --- a/crates/bevy_render/src/texture/texture_attachment.rs +++ b/crates/bevy_render/src/texture/texture_attachment.rs @@ -12,7 +12,7 @@ use wgpu::{ #[derive(Clone)] pub struct ColorAttachment { pub texture: CachedTexture, - pub resolve_target: Option, + pub multisampled: Option, pub previous_frame_texture: Option, clear_color: Option, is_first_call: Arc, @@ -21,13 +21,13 @@ pub struct ColorAttachment { impl ColorAttachment { pub fn new( texture: CachedTexture, - resolve_target: Option, + multisampled: Option, previous_frame_texture: Option, clear_color: Option, ) -> Self { Self { texture, - resolve_target, + multisampled, previous_frame_texture, clear_color, is_first_call: Arc::new(AtomicBool::new(true)), @@ -39,11 +39,11 @@ impl ColorAttachment { /// /// The returned attachment will always have writing enabled (`store: StoreOp::Load`). pub fn get_attachment(&self) -> RenderPassColorAttachment<'_> { - if let Some(resolve_target) = self.resolve_target.as_ref() { + if let Some(multisampled) = self.multisampled.as_ref() { let first_call = self.is_first_call.fetch_and(false, Ordering::SeqCst); RenderPassColorAttachment { - view: &resolve_target.default_view, + view: &multisampled.default_view, depth_slice: None, resolve_target: Some(&self.texture.default_view), ops: Operations { diff --git a/crates/bevy_render/src/view/mod.rs b/crates/bevy_render/src/view/mod.rs index 0895f2a9110ca..4e044649826c5 100644 --- a/crates/bevy_render/src/view/mod.rs +++ b/crates/bevy_render/src/view/mod.rs @@ -26,10 +26,10 @@ use crate::{ }; use alloc::sync::Arc; use bevy_app::{App, Plugin}; -use bevy_color::{LinearRgba, Oklaba}; +use bevy_color::{LinearRgba, Oklaba, Srgba}; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::prelude::*; -use bevy_image::ToExtents; +use bevy_image::{TextureSrgbViewFormats, ToExtents}; use bevy_math::{mat3, vec2, vec3, Mat3, Mat4, UVec4, Vec2, Vec3, Vec4, Vec4Swizzles}; use bevy_platform::collections::{hash_map::Entry, HashMap}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; @@ -826,6 +826,8 @@ impl ViewTarget { } /// The "main" unsampled texture. + /// + /// This is always non-srgb format in order to support storage binding. pub fn main_texture(&self) -> &Texture { if self.main_texture.load(Ordering::SeqCst) == 0 { &self.main_textures.a.texture.texture @@ -840,6 +842,8 @@ impl ViewTarget { /// /// A use case for this is to be able to prepare a bind group for all main textures /// ahead of time. + /// + /// This is always non-srgb format in order to support storage binding. pub fn main_texture_other(&self) -> &Texture { if self.main_texture.load(Ordering::SeqCst) == 0 { &self.main_textures.b.texture.texture @@ -848,7 +852,7 @@ impl ViewTarget { } } - /// The "main" unsampled texture. + /// The "main" unsampled texture view. pub fn main_texture_view(&self) -> &TextureView { if self.main_texture.load(Ordering::SeqCst) == 0 { &self.main_textures.a.texture.default_view @@ -875,7 +879,7 @@ impl ViewTarget { pub fn sampled_main_texture(&self) -> Option<&Texture> { self.main_textures .a - .resolve_target + .multisampled .as_ref() .map(|sampled| &sampled.texture) } @@ -884,16 +888,18 @@ impl ViewTarget { pub fn sampled_main_texture_view(&self) -> Option<&TextureView> { self.main_textures .a - .resolve_target + .multisampled .as_ref() .map(|sampled| &sampled.default_view) } - /// Currently bevy's main texture format can be: + /// Returns the format of [`Self::main_texture_view`]. + /// + /// Currently bevy's main texture view's format can be: /// - If rendering to screen: /// For HDR, it's `Rgba16Float`. - /// For LDR, it's `Rgba8Unorm` when [`CompositingSpace::Srgb`], otherwise `Rgba8UnormSrgb`. - /// - If rendering to texture: the format is the same as texture view's format. + /// For LDR, it's `Rgba8Unorm`. + /// - If rendering to texture: the format is the texture view's format but with srgb suffix removed. #[inline] pub fn main_texture_format(&self) -> TextureFormat { self.main_texture_format @@ -1167,25 +1173,19 @@ pub fn prepare_view_targets( }; // Convert clear color to the format expected by the main texture - let converted_clear_color: Option = clear_color.map(|color| { - let linear: LinearRgba = color.into(); - if camera - .compositing_space - .is_some_and(|s| s == CompositingSpace::Oklab) - { - // Main texture stores Oklab; convert linear RGB to Oklab for correct clear - let oklab: Oklaba = linear.into(); - oklab.into() - } else { - linear.into() - } - }); + let converted_clear_color: Option = + clear_color.map(|color| match camera.compositing_space { + // If main texture stores Oklab or Srgb, convert Color to it for correct clear. + Some(CompositingSpace::Oklab) => Oklaba::from(color).into(), + Some(CompositingSpace::Srgb) => Srgba::from(color).into(), + _ => LinearRgba::from(color).into(), + }); let (a, b, sampled, main_texture) = textures .entry(( camera.target.clone(), texture_usage.0, - main_texture_format, + view.target_format, msaa, )) .or_insert_with(|| { @@ -1197,11 +1197,8 @@ pub fn prepare_view_targets( dimension: TextureDimension::D2, format: main_texture_format, usage: texture_usage.0, - view_formats: match main_texture_format { - TextureFormat::Bgra8Unorm => &[TextureFormat::Bgra8UnormSrgb], - TextureFormat::Rgba8Unorm => &[TextureFormat::Rgba8UnormSrgb], - _ => &[], - }, + // `main_texture_format` is always non-srgb. We don't use srgb view but we allow it for users. + view_formats: main_texture_format.srgb_view_formats(), }; let a = texture_cache.get( &render_device, diff --git a/crates/bevy_render/src/view/window/mod.rs b/crates/bevy_render/src/view/window/mod.rs index ab422ee949e79..80815f7b8f518 100644 --- a/crates/bevy_render/src/view/window/mod.rs +++ b/crates/bevy_render/src/view/window/mod.rs @@ -394,9 +394,10 @@ pub fn create_surfaces( // Prefer sRGB formats for surfaces, but fall back to first available format if no sRGB formats are available. let mut format = *formats.first().expect("No supported formats for surface"); for available_format in formats { - // Rgba8UnormSrgb and Bgra8UnormSrgb and the only sRGB formats wgpu exposes that we can use for surfaces. - if available_format == TextureFormat::Rgba8UnormSrgb - || available_format == TextureFormat::Bgra8UnormSrgb + // Rgba8Unorm and Bgra8Unorm and the only formats that we can use for surfaces on WebGPU. + // Our renderer is in linear space and store the result to the srgb texture view. + if available_format == TextureFormat::Rgba8Unorm + || available_format == TextureFormat::Bgra8Unorm { format = available_format; break;