diff --git a/Cargo.toml b/Cargo.toml index a58240d53e6d1..023a4189688f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,6 +50,7 @@ default = [ "x11", "filesystem_watcher", "android_shared_stdcxx", + "tonemapping_luts" ] # Force dynamic linking, which improves iterative compile times @@ -78,6 +79,7 @@ trace = ["bevy_internal/trace"] wgpu_trace = ["bevy_internal/wgpu_trace"] # Image format support for texture loading (PNG and HDR are enabled by default) +exr = ["bevy_internal/exr"] hdr = ["bevy_internal/hdr"] png = ["bevy_internal/png"] tga = ["bevy_internal/tga"] @@ -131,6 +133,9 @@ android_shared_stdcxx = ["bevy_internal/android_shared_stdcxx"] # These trace events are expensive even when off, thus they require compile time opt-in. detailed_trace = ["bevy_internal/detailed_trace"] +# Include tonemapping LUT KTX2 files. +tonemapping_luts = ["bevy_internal/tonemapping_luts"] + [dependencies] bevy_dylib = { path = "crates/bevy_dylib", version = "0.9.0", default-features = false, optional = true } bevy_internal = { path = "crates/bevy_internal", version = "0.9.0", default-features = false } @@ -389,6 +394,17 @@ description = "Loads and renders a glTF file as a scene" category = "3D Rendering" wasm = true +[[example]] +name = "tonemapping" +path = "examples/3d/tonemapping.rs" +required-features = ["ktx2", "zstd"] + +[package.metadata.example.tonemapping] +name = "Tonemapping" +description = "Compares tonemapping options" +category = "3D Rendering" +wasm = true + [[example]] name = "fxaa" path = "examples/3d/fxaa.rs" diff --git a/assets/shaders/tonemapping_test_patterns.wgsl b/assets/shaders/tonemapping_test_patterns.wgsl new file mode 100644 index 0000000000000..cf5b025090730 --- /dev/null +++ b/assets/shaders/tonemapping_test_patterns.wgsl @@ -0,0 +1,64 @@ +#import bevy_pbr::mesh_view_bindings +#import bevy_pbr::mesh_bindings +#import bevy_pbr::utils + +#ifdef TONEMAP_IN_SHADER +#import bevy_core_pipeline::tonemapping +#endif + +struct FragmentInput { + @builtin(front_facing) is_front: bool, + @builtin(position) frag_coord: vec4, + #import bevy_pbr::mesh_vertex_output +}; + +// Sweep across hues on y axis with value from 0.0 to +15EV across x axis +// quantized into 24 steps for both axis. +fn color_sweep(uv: vec2) -> vec3 { + var uv = uv; + let steps = 24.0; + uv.y = uv.y * (1.0 + 1.0 / steps); + let ratio = 2.0; + + let h = PI * 2.0 * floor(1.0 + steps * uv.y) / steps; + let L = floor(uv.x * steps * ratio) / (steps * ratio) - 0.5; + + var color = vec3(0.0); + if uv.y < 1.0 { + color = cos(h + vec3(0.0, 1.0, 2.0) * PI * 2.0 / 3.0); + let maxRGB = max(color.r, max(color.g, color.b)); + let minRGB = min(color.r, min(color.g, color.b)); + color = exp(15.0 * L) * (color - minRGB) / (maxRGB - minRGB); + } else { + color = vec3(exp(15.0 * L)); + } + return color; +} + +fn hsv_to_srgb(c: vec3) -> vec3 { + let K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); + let p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www); + return c.z * mix(K.xxx, clamp(p - K.xxx, vec3(0.0), vec3(1.0)), c.y); +} + +// Generates a continuous sRGB sweep. +fn continuous_hue(uv: vec2) -> vec3 { + return hsv_to_srgb(vec3(uv.x, 1.0, 1.0)) * max(0.0, exp2(uv.y * 9.0) - 1.0); +} + +@fragment +fn fragment(in: FragmentInput) -> @location(0) vec4 { + var uv = in.uv; + var out = vec3(0.0); + if uv.y > 0.5 { + uv.y = 1.0 - uv.y; + out = color_sweep(vec2(uv.x, uv.y * 2.0)); + } else { + out = continuous_hue(vec2(uv.y * 2.0, uv.x)); + } + var color = vec4(out, 1.0); +#ifdef TONEMAP_IN_SHADER + color = tone_mapping(color); +#endif + return color; +} diff --git a/crates/bevy_core_pipeline/Cargo.toml b/crates/bevy_core_pipeline/Cargo.toml index af8c0b46f066c..c5c6978ac42e4 100644 --- a/crates/bevy_core_pipeline/Cargo.toml +++ b/crates/bevy_core_pipeline/Cargo.toml @@ -15,6 +15,7 @@ keywords = ["bevy"] [features] trace = [] webgl = [] +tonemapping_luts = [] [dependencies] # bevy diff --git a/crates/bevy_core_pipeline/src/core_2d/camera_2d.rs b/crates/bevy_core_pipeline/src/core_2d/camera_2d.rs index 0eb793228b9fa..36765180a15ad 100644 --- a/crates/bevy_core_pipeline/src/core_2d/camera_2d.rs +++ b/crates/bevy_core_pipeline/src/core_2d/camera_2d.rs @@ -1,4 +1,7 @@ -use crate::{clear_color::ClearColorConfig, tonemapping::Tonemapping}; +use crate::{ + clear_color::ClearColorConfig, + tonemapping::{DebandDither, Tonemapping}, +}; use bevy_ecs::prelude::*; use bevy_reflect::Reflect; use bevy_render::{ @@ -27,6 +30,7 @@ pub struct Camera2dBundle { pub global_transform: GlobalTransform, pub camera_2d: Camera2d, pub tonemapping: Tonemapping, + pub deband_dither: DebandDither, } impl Default for Camera2dBundle { @@ -67,7 +71,8 @@ impl Camera2dBundle { global_transform: Default::default(), camera: Camera::default(), camera_2d: Camera2d::default(), - tonemapping: Tonemapping::Disabled, + tonemapping: Tonemapping::None, + deband_dither: DebandDither::Disabled, } } } diff --git a/crates/bevy_core_pipeline/src/core_3d/camera_3d.rs b/crates/bevy_core_pipeline/src/core_3d/camera_3d.rs index 045ac906f7c7e..366ac2c22a54c 100644 --- a/crates/bevy_core_pipeline/src/core_3d/camera_3d.rs +++ b/crates/bevy_core_pipeline/src/core_3d/camera_3d.rs @@ -1,4 +1,7 @@ -use crate::{clear_color::ClearColorConfig, tonemapping::Tonemapping}; +use crate::{ + clear_color::ClearColorConfig, + tonemapping::{DebandDither, Tonemapping}, +}; use bevy_ecs::prelude::*; use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize}; use bevy_render::{ @@ -6,7 +9,7 @@ use bevy_render::{ extract_component::ExtractComponent, primitives::Frustum, render_resource::LoadOp, - view::VisibleEntities, + view::{ColorGrading, VisibleEntities}, }; use bevy_transform::prelude::{GlobalTransform, Transform}; use serde::{Deserialize, Serialize}; @@ -59,6 +62,8 @@ pub struct Camera3dBundle { pub global_transform: GlobalTransform, pub camera_3d: Camera3d, pub tonemapping: Tonemapping, + pub dither: DebandDither, + pub color_grading: ColorGrading, } // NOTE: ideally Perspective and Orthographic defaults can share the same impl, but sadly it breaks rust's type inference @@ -66,9 +71,6 @@ impl Default for Camera3dBundle { fn default() -> Self { Self { camera_render_graph: CameraRenderGraph::new(crate::core_3d::graph::NAME), - tonemapping: Tonemapping::Enabled { - deband_dither: true, - }, camera: Default::default(), projection: Default::default(), visible_entities: Default::default(), @@ -76,6 +78,9 @@ impl Default for Camera3dBundle { transform: Default::default(), global_transform: Default::default(), camera_3d: Default::default(), + tonemapping: Tonemapping::ReinhardLuminance, + dither: DebandDither::Enabled, + color_grading: ColorGrading::default(), } } } diff --git a/crates/bevy_core_pipeline/src/tonemapping/luts/AgX-default_contrast.ktx2 b/crates/bevy_core_pipeline/src/tonemapping/luts/AgX-default_contrast.ktx2 new file mode 100644 index 0000000000000..040fb1def2383 Binary files /dev/null and b/crates/bevy_core_pipeline/src/tonemapping/luts/AgX-default_contrast.ktx2 differ diff --git a/crates/bevy_core_pipeline/src/tonemapping/luts/Blender_-11_12.ktx2 b/crates/bevy_core_pipeline/src/tonemapping/luts/Blender_-11_12.ktx2 new file mode 100644 index 0000000000000..db07c847a1879 Binary files /dev/null and b/crates/bevy_core_pipeline/src/tonemapping/luts/Blender_-11_12.ktx2 differ diff --git a/crates/bevy_core_pipeline/src/tonemapping/luts/info.txt b/crates/bevy_core_pipeline/src/tonemapping/luts/info.txt new file mode 100644 index 0000000000000..e3b6b8a17f429 --- /dev/null +++ b/crates/bevy_core_pipeline/src/tonemapping/luts/info.txt @@ -0,0 +1,22 @@ +--- Process for recreating AgX-default_contrast.ktx2 --- +Download: +https://github.com/MrLixm/AgXc/blob/898198e0490b0551ed81412a0c22e0b72fffb7cd/obs/obs-script/AgX-default_contrast.lut.png +Convert to vertical strip exr with: +https://gist.github.com/DGriffin91/fc8e0cfd55aaa175ac10199403bc19b8 +Convert exr to 3D ktx2 with: +https://gist.github.com/DGriffin91/49401c43378b58bce32059291097d4ca + +--- Process for recreating tony_mc_mapface.ktx2 --- +Download: +https://github.com/h3r2tic/tony-mc-mapface/blob/909e51c8a74251fd828770248476cb084081e08c/tony_mc_mapface.dds +Convert dds to 3D ktx2 with: +https://gist.github.com/DGriffin91/49401c43378b58bce32059291097d4ca + +--- Process for recreating Blender_-11_12.ktx2 --- +Create LUT stimulus with: +https://gist.github.com/DGriffin91/e119bf32b520e219f6e102a6eba4a0cf +Open LUT image in Blender's image editor and make sure color space is set to linear. +Export from Blender as 32bit EXR, override color space to Filmic sRGB. +Import EXR back into blender set color space to sRGB, then export as 32bit EXR override color space to linear. +Convert exr to 3D ktx2 with: +https://gist.github.com/DGriffin91/49401c43378b58bce32059291097d4ca diff --git a/crates/bevy_core_pipeline/src/tonemapping/luts/tony_mc_mapface.ktx2 b/crates/bevy_core_pipeline/src/tonemapping/luts/tony_mc_mapface.ktx2 new file mode 100644 index 0000000000000..143759429e946 Binary files /dev/null and b/crates/bevy_core_pipeline/src/tonemapping/luts/tony_mc_mapface.ktx2 differ diff --git a/crates/bevy_core_pipeline/src/tonemapping/mod.rs b/crates/bevy_core_pipeline/src/tonemapping/mod.rs index 104ff928d130d..a4394d7778911 100644 --- a/crates/bevy_core_pipeline/src/tonemapping/mod.rs +++ b/crates/bevy_core_pipeline/src/tonemapping/mod.rs @@ -1,16 +1,20 @@ use crate::fullscreen_vertex_shader::fullscreen_shader_vertex_state; use bevy_app::prelude::*; -use bevy_asset::{load_internal_asset, HandleUntyped}; +use bevy_asset::{load_internal_asset, Assets, Handle, HandleUntyped}; use bevy_ecs::prelude::*; -use bevy_reflect::{Reflect, TypeUuid}; +use bevy_reflect::{FromReflect, Reflect, TypeUuid}; use bevy_render::camera::Camera; use bevy_render::extract_component::{ExtractComponent, ExtractComponentPlugin}; +use bevy_render::extract_resource::{ExtractResource, ExtractResourcePlugin}; +use bevy_render::render_asset::RenderAssets; use bevy_render::renderer::RenderDevice; -use bevy_render::view::ViewTarget; +use bevy_render::texture::{CompressedImageFormats, Image, ImageSampler, ImageType}; +use bevy_render::view::{ViewTarget, ViewUniform}; use bevy_render::{render_resource::*, RenderApp, RenderSet}; mod node; +use bevy_utils::default; pub use node::TonemappingNode; const TONEMAPPING_SHADER_HANDLE: HandleUntyped = @@ -19,6 +23,14 @@ const TONEMAPPING_SHADER_HANDLE: HandleUntyped = const TONEMAPPING_SHARED_SHADER_HANDLE: HandleUntyped = HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 2499430578245347910); +/// 3D LUT (look up table) textures used for tonemapping +#[derive(Resource, Clone, ExtractResource)] +pub struct TonemappingLuts { + blender_filmic: Handle, + agx: Handle, + tony_mc_mapface: Handle, +} + pub struct TonemappingPlugin; impl Plugin for TonemappingPlugin { @@ -36,9 +48,47 @@ impl Plugin for TonemappingPlugin { Shader::from_wgsl ); + if !app.world.is_resource_added::() { + let mut images = app.world.resource_mut::>(); + + #[cfg(feature = "tonemapping_luts")] + let tonemapping_luts = { + TonemappingLuts { + blender_filmic: images.add(setup_tonemapping_lut_image( + include_bytes!("luts/Blender_-11_12.ktx2"), + ImageType::Extension("ktx2"), + )), + agx: images.add(setup_tonemapping_lut_image( + include_bytes!("luts/AgX-default_contrast.ktx2"), + ImageType::Extension("ktx2"), + )), + tony_mc_mapface: images.add(setup_tonemapping_lut_image( + include_bytes!("luts/tony_mc_mapface.ktx2"), + ImageType::Extension("ktx2"), + )), + } + }; + + #[cfg(not(feature = "tonemapping_luts"))] + let tonemapping_luts = { + let placeholder = images.add(lut_placeholder()); + TonemappingLuts { + blender_filmic: placeholder.clone(), + agx: placeholder.clone(), + tony_mc_mapface: placeholder, + } + }; + + app.insert_resource(tonemapping_luts); + } + + app.add_plugin(ExtractResourcePlugin::::default()); + app.register_type::(); + app.register_type::(); app.add_plugin(ExtractComponentPlugin::::default()); + app.add_plugin(ExtractComponentPlugin::::default()); if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { render_app @@ -54,9 +104,77 @@ pub struct TonemappingPipeline { texture_bind_group: BindGroupLayout, } -#[derive(Copy, Clone, PartialEq, Eq, Hash)] +/// Optionally enables a tonemapping shader that attempts to map linear input stimulus into a perceptually uniform image for a given [`Camera`] entity. +#[derive( + Component, + Debug, + Hash, + Clone, + Copy, + Reflect, + Default, + ExtractComponent, + PartialEq, + Eq, + FromReflect, +)] +#[extract_component_filter(With)] +#[reflect(Component)] +pub enum Tonemapping { + /// Bypass tonemapping. + None, + /// Suffers from lots hue shifting, brights don't desaturate naturally. + /// Bright primaries and secondaries don't desaturate at all. + Reinhard, + /// Current bevy default. Likely to change in the future. + /// Suffers from hue shifting. Brights don't desaturate much at all across the spectrum. + #[default] + ReinhardLuminance, + /// Same base implementation that Godot 4.0 uses for Tonemap ACES. + /// + /// Not neutral, has a very specific aesthetic, intentional and dramatic hue shifting. + /// Bright greens and reds turn orange. Bright blues turn magenta. + /// Significantly increased contrast. Brights desaturate across the spectrum. + AcesFitted, + /// By Troy Sobotka + /// + /// Very neutral. Image is somewhat desaturated when compared to other tonemappers. + /// Little to no hue shifting. Subtle [Abney shifting](https://en.wikipedia.org/wiki/Abney_effect). + /// NOTE: Requires the `tonemapping_luts` cargo feature. + AgX, + /// By Tomasz Stachowiak + /// Has little hue shifting in the darks and mids, but lots in the brights. Brights desaturate across the spectrum. + /// Is sort of between Reinhard and ReinhardLuminance. Conceptually similar to reinhard-jodie. + /// Designed as a compromise if you want e.g. decent skin tones in low light, but can't afford to re-do your + /// VFX to look good without hue shifting. + SomewhatBoringDisplayTransform, + /// By Tomasz Stachowiak + /// + /// Very neutral. Subtle but intentional hue shifting. Brights desaturate across the spectrum. + /// Comment from author: + /// Tony is a display transform intended for real-time applications such as games. + /// It is intentionally boring, does not increase contrast or saturation, and stays close to the + /// input stimulus where compression isn't necessary. + /// Brightness-equivalent luminance of the input stimulus is compressed. The non-linearity resembles Reinhard. + /// Color hues are preserved during compression, except for a deliberate [Bezold–Brücke shift](https://en.wikipedia.org/wiki/Bezold%E2%80%93Br%C3%BCcke_shift). + /// To avoid posterization, selective desaturation is employed, with care to avoid the [Abney effect](https://en.wikipedia.org/wiki/Abney_effect). + /// NOTE: Requires the `tonemapping_luts` cargo feature. + TonyMcMapface, + /// Default Filmic Display Transform from blender. + /// Somewhat neutral. Suffers from hue shifting. Brights desaturate across the spectrum. + /// NOTE: Requires the `tonemapping_luts` cargo feature. + BlenderFilmic, +} + +impl Tonemapping { + pub fn is_enabled(&self) -> bool { + *self != Tonemapping::None + } +} +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] pub struct TonemappingPipelineKey { - deband_dither: bool, + deband_dither: DebandDither, + tonemapping: Tonemapping, } impl SpecializedRenderPipeline for TonemappingPipeline { @@ -64,9 +182,25 @@ impl SpecializedRenderPipeline for TonemappingPipeline { fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor { let mut shader_defs = Vec::new(); - if key.deband_dither { + if let DebandDither::Enabled = key.deband_dither { shader_defs.push("DEBAND_DITHER".into()); } + match key.tonemapping { + Tonemapping::None => shader_defs.push("TONEMAP_METHOD_NONE".into()), + Tonemapping::Reinhard => shader_defs.push("TONEMAP_METHOD_REINHARD".into()), + Tonemapping::ReinhardLuminance => { + shader_defs.push("TONEMAP_METHOD_REINHARD_LUMINANCE".into()); + } + Tonemapping::AcesFitted => shader_defs.push("TONEMAP_METHOD_ACES_FITTED".into()), + Tonemapping::AgX => shader_defs.push("TONEMAP_METHOD_AGX".into()), + Tonemapping::SomewhatBoringDisplayTransform => { + shader_defs.push("TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM".into()); + } + Tonemapping::TonyMcMapface => shader_defs.push("TONEMAP_METHOD_TONY_MC_MAPFACE".into()), + Tonemapping::BlenderFilmic => { + shader_defs.push("TONEMAP_METHOD_BLENDER_FILMIC".into()); + } + } RenderPipelineDescriptor { label: Some("tonemapping pipeline".into()), layout: vec![self.texture_bind_group.clone()], @@ -91,28 +225,41 @@ impl SpecializedRenderPipeline for TonemappingPipeline { impl FromWorld for TonemappingPipeline { fn from_world(render_world: &mut World) -> Self { + let mut entries = vec![ + BindGroupLayoutEntry { + binding: 0, + visibility: ShaderStages::FRAGMENT, + ty: BindingType::Buffer { + ty: BufferBindingType::Uniform, + has_dynamic_offset: true, + min_binding_size: Some(ViewUniform::min_size()), + }, + count: None, + }, + BindGroupLayoutEntry { + binding: 1, + visibility: ShaderStages::FRAGMENT, + ty: BindingType::Texture { + sample_type: TextureSampleType::Float { filterable: false }, + view_dimension: TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }, + BindGroupLayoutEntry { + binding: 2, + visibility: ShaderStages::FRAGMENT, + ty: BindingType::Sampler(SamplerBindingType::NonFiltering), + count: None, + }, + ]; + entries.extend(get_lut_bind_group_layout_entries([3, 4])); + let tonemap_texture_bind_group = render_world .resource::() .create_bind_group_layout(&BindGroupLayoutDescriptor { label: Some("tonemapping_hdr_texture_bind_group_layout"), - entries: &[ - BindGroupLayoutEntry { - binding: 0, - visibility: ShaderStages::FRAGMENT, - ty: BindingType::Texture { - sample_type: TextureSampleType::Float { filterable: false }, - view_dimension: TextureViewDimension::D2, - multisampled: false, - }, - count: None, - }, - BindGroupLayoutEntry { - binding: 1, - visibility: ShaderStages::FRAGMENT, - ty: BindingType::Sampler(SamplerBindingType::NonFiltering), - count: None, - }, - ], + entries: &entries, }); TonemappingPipeline { @@ -129,35 +276,133 @@ pub fn queue_view_tonemapping_pipelines( pipeline_cache: Res, mut pipelines: ResMut>, upscaling_pipeline: Res, - view_targets: Query<(Entity, &Tonemapping)>, + view_targets: Query<(Entity, Option<&Tonemapping>, Option<&DebandDither>), With>, ) { - for (entity, tonemapping) in view_targets.iter() { - if let Tonemapping::Enabled { deband_dither } = tonemapping { - let key = TonemappingPipelineKey { - deband_dither: *deband_dither, - }; - let pipeline = pipelines.specialize(&pipeline_cache, &upscaling_pipeline, key); + for (entity, tonemapping, dither) in view_targets.iter() { + let key = TonemappingPipelineKey { + deband_dither: *dither.unwrap_or(&DebandDither::Disabled), + tonemapping: *tonemapping.unwrap_or(&Tonemapping::None), + }; + let pipeline = pipelines.specialize(&pipeline_cache, &upscaling_pipeline, key); - commands - .entity(entity) - .insert(ViewTonemappingPipeline(pipeline)); - } + commands + .entity(entity) + .insert(ViewTonemappingPipeline(pipeline)); } } - -#[derive(Component, Clone, Reflect, Default, ExtractComponent)] +/// Enables a debanding shader that applies dithering to mitigate color banding in the final image for a given [`Camera`] entity. +#[derive( + Component, + Debug, + Hash, + Clone, + Copy, + Reflect, + Default, + ExtractComponent, + PartialEq, + Eq, + FromReflect, +)] #[extract_component_filter(With)] #[reflect(Component)] -pub enum Tonemapping { +pub enum DebandDither { #[default] Disabled, - Enabled { - deband_dither: bool, - }, + Enabled, } -impl Tonemapping { - pub fn is_enabled(&self) -> bool { - matches!(self, Tonemapping::Enabled { .. }) +pub fn get_lut_bindings<'a>( + images: &'a RenderAssets, + tonemapping_luts: &'a TonemappingLuts, + tonemapping: &Tonemapping, + bindings: [u32; 2], +) -> [BindGroupEntry<'a>; 2] { + let image = match tonemapping { + //AgX lut texture used when tonemapping doesn't need a texture since it's very small (32x32x32) + Tonemapping::None + | Tonemapping::Reinhard + | Tonemapping::ReinhardLuminance + | Tonemapping::AcesFitted + | Tonemapping::AgX + | Tonemapping::SomewhatBoringDisplayTransform => &tonemapping_luts.agx, + Tonemapping::TonyMcMapface => &tonemapping_luts.tony_mc_mapface, + Tonemapping::BlenderFilmic => &tonemapping_luts.blender_filmic, + }; + let lut_image = images.get(image).unwrap(); + [ + BindGroupEntry { + binding: bindings[0], + resource: BindingResource::TextureView(&lut_image.texture_view), + }, + BindGroupEntry { + binding: bindings[1], + resource: BindingResource::Sampler(&lut_image.sampler), + }, + ] +} + +pub fn get_lut_bind_group_layout_entries(bindings: [u32; 2]) -> [BindGroupLayoutEntry; 2] { + [ + BindGroupLayoutEntry { + binding: bindings[0], + visibility: ShaderStages::FRAGMENT, + ty: BindingType::Texture { + sample_type: TextureSampleType::Float { filterable: true }, + view_dimension: TextureViewDimension::D3, + multisampled: false, + }, + count: None, + }, + BindGroupLayoutEntry { + binding: bindings[1], + visibility: ShaderStages::FRAGMENT, + ty: BindingType::Sampler(SamplerBindingType::Filtering), + count: None, + }, + ] +} + +// allow(dead_code) so it doesn't complain when the tonemapping_luts feature is disabled +#[allow(dead_code)] +fn setup_tonemapping_lut_image(bytes: &[u8], image_type: ImageType) -> Image { + let mut image = + Image::from_buffer(bytes, image_type, CompressedImageFormats::NONE, false).unwrap(); + + image.sampler_descriptor = bevy_render::texture::ImageSampler::Descriptor(SamplerDescriptor { + label: Some("Tonemapping LUT sampler"), + address_mode_u: AddressMode::ClampToEdge, + address_mode_v: AddressMode::ClampToEdge, + address_mode_w: AddressMode::ClampToEdge, + mag_filter: FilterMode::Linear, + min_filter: FilterMode::Linear, + mipmap_filter: FilterMode::Linear, + ..default() + }); + + image +} + +pub fn lut_placeholder() -> Image { + let format = TextureFormat::Rgba8Unorm; + let data = vec![255, 0, 255, 255]; + Image { + data, + texture_descriptor: TextureDescriptor { + size: Extent3d { + width: 1, + height: 1, + depth_or_array_layers: 1, + }, + format, + dimension: TextureDimension::D3, + label: None, + mip_level_count: 1, + sample_count: 1, + usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST, + view_formats: &[], + }, + sampler_descriptor: ImageSampler::Default, + texture_view_descriptor: None, } } diff --git a/crates/bevy_core_pipeline/src/tonemapping/node.rs b/crates/bevy_core_pipeline/src/tonemapping/node.rs index c814de5c00ed7..09690a293e37c 100644 --- a/crates/bevy_core_pipeline/src/tonemapping/node.rs +++ b/crates/bevy_core_pipeline/src/tonemapping/node.rs @@ -1,9 +1,11 @@ use std::sync::Mutex; -use crate::tonemapping::{TonemappingPipeline, ViewTonemappingPipeline}; +use crate::tonemapping::{TonemappingLuts, TonemappingPipeline, ViewTonemappingPipeline}; + use bevy_ecs::prelude::*; use bevy_ecs::query::QueryState; use bevy_render::{ + render_asset::RenderAssets, render_graph::{Node, NodeRunError, RenderGraphContext, SlotInfo, SlotType}, render_resource::{ BindGroup, BindGroupDescriptor, BindGroupEntry, BindingResource, LoadOp, Operations, @@ -11,12 +13,24 @@ use bevy_render::{ TextureViewId, }, renderer::RenderContext, - view::{ExtractedView, ViewTarget}, + texture::Image, + view::{ExtractedView, ViewTarget, ViewUniformOffset, ViewUniforms}, }; +use super::{get_lut_bindings, Tonemapping}; + pub struct TonemappingNode { - query: QueryState<(&'static ViewTarget, &'static ViewTonemappingPipeline), With>, + query: QueryState< + ( + &'static ViewUniformOffset, + &'static ViewTarget, + &'static ViewTonemappingPipeline, + &'static Tonemapping, + ), + With, + >, cached_texture_bind_group: Mutex>, + last_tonemapping: Mutex>, } impl TonemappingNode { @@ -26,6 +40,7 @@ impl TonemappingNode { Self { query: QueryState::new(world), cached_texture_bind_group: Mutex::new(None), + last_tonemapping: Mutex::new(None), } } } @@ -48,17 +63,21 @@ impl Node for TonemappingNode { let view_entity = graph.get_input_entity(Self::IN_VIEW)?; let pipeline_cache = world.resource::(); let tonemapping_pipeline = world.resource::(); + let gpu_images = world.get_resource::>().unwrap(); + let view_uniforms_resource = world.resource::(); + let view_uniforms = view_uniforms_resource.uniforms.binding().unwrap(); - let (target, tonemapping) = match self.query.get_manual(world, view_entity) { - Ok(result) => result, - Err(_) => return Ok(()), - }; + let (view_uniform_offset, target, view_tonemapping_pipeline, tonemapping) = + match self.query.get_manual(world, view_entity) { + Ok(result) => result, + Err(_) => return Ok(()), + }; if !target.is_hdr() { return Ok(()); } - let pipeline = match pipeline_cache.get_render_pipeline(tonemapping.0) { + let pipeline = match pipeline_cache.get_render_pipeline(view_tonemapping_pipeline.0) { Some(pipeline) => pipeline, None => return Ok(()), }; @@ -67,30 +86,56 @@ impl Node for TonemappingNode { let source = post_process.source; let destination = post_process.destination; + let mut last_tonemapping = self.last_tonemapping.lock().unwrap(); + + let tonemapping_changed = if let Some(last_tonemapping) = &*last_tonemapping { + tonemapping != last_tonemapping + } else { + true + }; + if tonemapping_changed { + *last_tonemapping = Some(*tonemapping); + } + let mut cached_bind_group = self.cached_texture_bind_group.lock().unwrap(); let bind_group = match &mut *cached_bind_group { - Some((id, bind_group)) if source.id() == *id => bind_group, + Some((id, bind_group)) if source.id() == *id && !tonemapping_changed => bind_group, cached_bind_group => { let sampler = render_context .render_device() .create_sampler(&SamplerDescriptor::default()); + let tonemapping_luts = world.resource::(); + + let mut entries = vec![ + BindGroupEntry { + binding: 0, + resource: view_uniforms.clone(), + }, + BindGroupEntry { + binding: 1, + resource: BindingResource::TextureView(source), + }, + BindGroupEntry { + binding: 2, + resource: BindingResource::Sampler(&sampler), + }, + ]; + + entries.extend(get_lut_bindings( + gpu_images, + tonemapping_luts, + tonemapping, + [3, 4], + )); + let bind_group = render_context .render_device() .create_bind_group(&BindGroupDescriptor { label: None, layout: &tonemapping_pipeline.texture_bind_group, - entries: &[ - BindGroupEntry { - binding: 0, - resource: BindingResource::TextureView(source), - }, - BindGroupEntry { - binding: 1, - resource: BindingResource::Sampler(&sampler), - }, - ], + entries: &entries, }); let (_, bind_group) = cached_bind_group.insert((source.id(), bind_group)); @@ -116,7 +161,7 @@ impl Node for TonemappingNode { .begin_render_pass(&pass_descriptor); render_pass.set_pipeline(pipeline); - render_pass.set_bind_group(0, bind_group, &[]); + render_pass.set_bind_group(0, bind_group, &[view_uniform_offset.offset]); render_pass.draw(0..3, 0..1); Ok(()) diff --git a/crates/bevy_core_pipeline/src/tonemapping/tonemapping.wgsl b/crates/bevy_core_pipeline/src/tonemapping/tonemapping.wgsl index a4bc5d4be4364..fe0d4e5684572 100644 --- a/crates/bevy_core_pipeline/src/tonemapping/tonemapping.wgsl +++ b/crates/bevy_core_pipeline/src/tonemapping/tonemapping.wgsl @@ -1,23 +1,33 @@ #import bevy_core_pipeline::fullscreen_vertex_shader -#import bevy_core_pipeline::tonemapping + +#import bevy_render::view @group(0) @binding(0) -var hdr_texture: texture_2d; +var view: View; + @group(0) @binding(1) +var hdr_texture: texture_2d; +@group(0) @binding(2) var hdr_sampler: sampler; +@group(0) @binding(3) +var dt_lut_texture: texture_3d; +@group(0) @binding(4) +var dt_lut_sampler: sampler; + +#import bevy_core_pipeline::tonemapping @fragment fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4 { let hdr_color = textureSample(hdr_texture, hdr_sampler, in.uv); - var output_rgb = reinhard_luminance(hdr_color.rgb); + var output_rgb = tone_mapping(hdr_color).rgb; #ifdef DEBAND_DITHER - output_rgb = pow(output_rgb.rgb, vec3(1.0 / 2.2)); + output_rgb = powsafe(output_rgb.rgb, 1.0 / 2.2); output_rgb = output_rgb + screen_space_dither(in.position.xy); // This conversion back to linear space is required because our output texture format is // SRGB; the GPU will assume our output is linear and will apply an SRGB conversion. - output_rgb = pow(output_rgb.rgb, vec3(2.2)); + output_rgb = powsafe(output_rgb.rgb, 2.2); #endif return vec4(output_rgb, hdr_color.a); diff --git a/crates/bevy_core_pipeline/src/tonemapping/tonemapping_shared.wgsl b/crates/bevy_core_pipeline/src/tonemapping/tonemapping_shared.wgsl index f538af1b2d812..f78a420622981 100644 --- a/crates/bevy_core_pipeline/src/tonemapping/tonemapping_shared.wgsl +++ b/crates/bevy_core_pipeline/src/tonemapping/tonemapping_shared.wgsl @@ -1,5 +1,235 @@ #define_import_path bevy_core_pipeline::tonemapping + +fn sample_current_lut(p: vec3) -> vec3 { + // Don't include code that will try to sample from LUTs if tonemap method doesn't require it + // Allows this file to be imported without necessarily needing the lut texture bindings +#ifdef TONEMAP_METHOD_AGX + return textureSampleLevel(dt_lut_texture, dt_lut_sampler, p, 0.0).rgb; +#else ifdef TONEMAP_METHOD_TONY_MC_MAPFACE + return textureSampleLevel(dt_lut_texture, dt_lut_sampler, p, 0.0).rgb; +#else ifdef TONEMAP_METHOD_BLENDER_FILMIC + return textureSampleLevel(dt_lut_texture, dt_lut_sampler, p, 0.0).rgb; +#else + return vec3(1.0, 0.0, 1.0); + #endif +} + +// -------------------------------------- +// --- SomewhatBoringDisplayTransform --- +// -------------------------------------- +// By Tomasz Stachowiak + +fn rgb_to_ycbcr(col: vec3) -> vec3 { + let m = mat3x3( + 0.2126, 0.7152, 0.0722, + -0.1146, -0.3854, 0.5, + 0.5, -0.4542, -0.0458 + ); + return col * m; +} + +fn ycbcr_to_rgb(col: vec3) -> vec3 { + let m = mat3x3( + 1.0, 0.0, 1.5748, + 1.0, -0.1873, -0.4681, + 1.0, 1.8556, 0.0 + ); + return max(vec3(0.0), col * m); +} + +fn tonemap_curve(v: f32) -> f32 { +#ifdef 0 + // Large linear part in the lows, but compresses highs. + float c = v + v * v + 0.5 * v * v * v; + return c / (1.0 + c); +#else + return 1.0 - exp(-v); +#endif +} + +fn tonemap_curve3(v: vec3) -> vec3 { + return vec3(tonemap_curve(v.r), tonemap_curve(v.g), tonemap_curve(v.b)); +} + +fn somewhat_boring_display_transform(col: vec3) -> vec3 { + var col = col; + let ycbcr = rgb_to_ycbcr(col); + + let bt = tonemap_curve(length(ycbcr.yz) * 2.4); + var desat = max((bt - 0.7) * 0.8, 0.0); + desat *= desat; + + let desat_col = mix(col.rgb, ycbcr.xxx, desat); + + let tm_luma = tonemap_curve(ycbcr.x); + let tm0 = col.rgb * max(0.0, tm_luma / max(1e-5, tonemapping_luminance(col.rgb))); + let final_mult = 0.97; + let tm1 = tonemap_curve3(desat_col); + + col = mix(tm0, tm1, bt * bt); + + return col * final_mult; +} + +// ------------------------------------------ +// ------------- Tony McMapface ------------- +// ------------------------------------------ +// By Tomasz Stachowiak +// https://github.com/h3r2tic/tony-mc-mapface + +const TONY_MC_MAPFACE_LUT_EV_RANGE = vec2(-13.0, 8.0); +const TONY_MC_MAPFACE_LUT_DIMS: f32 = 48.0; + +fn tony_mc_mapface_lut_range_encode(x: vec3) -> vec3 { + return x / (x + 1.0); +} + +fn sample_tony_mc_mapface_lut(stimulus: vec3) -> vec3 { + let range = tony_mc_mapface_lut_range_encode(exp2(TONY_MC_MAPFACE_LUT_EV_RANGE.xyy)).xy; + let normalized = (tony_mc_mapface_lut_range_encode(stimulus) - range.x) / (range.y - range.x); + var uv = saturate(normalized * (f32(TONY_MC_MAPFACE_LUT_DIMS - 1.0) / f32(TONY_MC_MAPFACE_LUT_DIMS)) + 0.5 / f32(TONY_MC_MAPFACE_LUT_DIMS)); + return sample_current_lut(uv).rgb; +} + +// --------------------------------- +// ---------- ACES Fitted ---------- +// --------------------------------- + +// Same base implementation that Godot 4.0 uses for Tonemap ACES. + +// https://github.com/TheRealMJP/BakingLab/blob/master/BakingLab/ACES.hlsl + +// The code in this file was originally written by Stephen Hill (@self_shadow), who deserves all +// credit for coming up with this fit and implementing it. Buy him a beer next time you see him. :) + +fn RRTAndODTFit(v: vec3) -> vec3 { + let a = v * (v + 0.0245786) - 0.000090537; + let b = v * (0.983729 * v + 0.4329510) + 0.238081; + return a / b; +} + +fn ACESFitted(color: vec3) -> vec3 { + var color = color; + + // sRGB => XYZ => D65_2_D60 => AP1 => RRT_SAT + let rgb_to_rrt = mat3x3( + vec3(0.59719, 0.35458, 0.04823), + vec3(0.07600, 0.90834, 0.01566), + vec3(0.02840, 0.13383, 0.83777) + ); + + // ODT_SAT => XYZ => D60_2_D65 => sRGB + let odt_to_rgb = mat3x3( + vec3(1.60475, -0.53108, -0.07367), + vec3(-0.10208, 1.10813, -0.00605), + vec3(-0.00327, -0.07276, 1.07602) + ); + + color *= rgb_to_rrt; + + // Apply RRT and ODT + color = RRTAndODTFit(color); + + color *= odt_to_rgb; + + // Clamp to [0, 1] + color = saturate(color); + + return color; +} + +// ------------------------------- +// ------------- AgX ------------- +// ------------------------------- +// By Troy Sobotka +// https://github.com/MrLixm/AgXc +// https://github.com/sobotka/AgX + +// pow() but safe for NaNs/negatives +fn powsafe(color: vec3, power: f32) -> vec3 { + return pow(abs(color), vec3(power)) * sign(color); +} + +/* + Increase color saturation of the given color data. + :param color: expected sRGB primaries input + :param saturationAmount: expected 0-1 range with 1=neutral, 0=no saturation. + -- ref[2] [4] +*/ +fn saturation(color: vec3, saturationAmount: f32) -> vec3 { + let luma = tonemapping_luminance(color); + return mix(vec3(luma), color, vec3(saturationAmount)); +} + +/* + Output log domain encoded data. + Similar to OCIO lg2 AllocationTransform. + ref[0] +*/ +fn convertOpenDomainToNormalizedLog2(color: vec3, minimum_ev: f32, maximum_ev: f32) -> vec3 { + let in_midgrey = 0.18; + + // remove negative before log transform + var color = max(vec3(0.0), color); + // avoid infinite issue with log -- ref[1] + color = select(color, 0.00001525878 + color, color < 0.00003051757); + color = clamp( + log2(color / in_midgrey), + vec3(minimum_ev), + vec3(maximum_ev) + ); + let total_exposure = maximum_ev - minimum_ev; + + return (color - minimum_ev) / total_exposure; +} + +// Inverse of above +fn convertNormalizedLog2ToOpenDomain(color: vec3, minimum_ev: f32, maximum_ev: f32) -> vec3 { + var color = color; + let in_midgrey = 0.18; + let total_exposure = maximum_ev - minimum_ev; + + color = (color * total_exposure) + minimum_ev; + color = pow(vec3(2.0), color); + color = color * in_midgrey; + + return color; +} + + +/*================= + Main processes +=================*/ + +// Prepare the data for display encoding. Converted to log domain. +fn applyAgXLog(Image: vec3) -> vec3 { + var Image = max(vec3(0.0), Image); // clamp negatives + let r = dot(Image, vec3(0.84247906, 0.0784336, 0.07922375)); + let g = dot(Image, vec3(0.04232824, 0.87846864, 0.07916613)); + let b = dot(Image, vec3(0.04237565, 0.0784336, 0.87914297)); + Image = vec3(r, g, b); + + Image = convertOpenDomainToNormalizedLog2(Image, -10.0, 6.5); + + Image = clamp(Image, vec3(0.0), vec3(1.0)); + return Image; +} + +fn applyLUT3D(Image: vec3, block_size: f32) -> vec3 { + return sample_current_lut(Image * ((block_size - 1.0) / block_size) + 0.5 / block_size).rgb; +} + +// ------------------------- +// ------------------------- +// ------------------------- + +fn sample_blender_filmic_lut(stimulus: vec3) -> vec3 { + let block_size = 64.0; + let normalized = saturate(convertOpenDomainToNormalizedLog2(stimulus, -11.0, 12.0)); + return applyLUT3D(normalized, block_size); +} + // from https://64.github.io/tonemapping/ // reinhard on RGB oversaturates colors fn tonemapping_reinhard(color: vec3) -> vec3 { @@ -22,7 +252,7 @@ fn tonemapping_change_luminance(c_in: vec3, l_out: f32) -> vec3 { return c_in * (l_out / l_in); } -fn reinhard_luminance(color: vec3) -> vec3 { +fn tonemapping_reinhard_luminance(color: vec3) -> vec3 { let l_old = tonemapping_luminance(color); let l_new = l_old / (1.0 + l_old); return tonemapping_change_luminance(color, l_new); @@ -35,3 +265,47 @@ fn screen_space_dither(frag_coord: vec2) -> vec3 { dither = fract(dither.rgb / vec3(103.0, 71.0, 97.0)); return (dither - 0.5) / 255.0; } + +fn tone_mapping(in: vec4) -> vec4 { + var color = max(in.rgb, vec3(0.0)); + + // Possible future grading: + + // highlight gain gamma: 0.. + // let luma = powsafe(vec3(tonemapping_luminance(color)), 1.0); + + // highlight gain: 0.. + // color += color * luma.xxx * 1.0; + + // Linear pre tonemapping grading + color = saturation(color, view.color_grading.pre_saturation); + color = powsafe(color, view.color_grading.gamma); + color = color * powsafe(vec3(2.0), view.color_grading.exposure); + color = max(color, vec3(0.0)); + + // tone_mapping +#ifdef TONEMAP_METHOD_NONE + color = color; +#else ifdef TONEMAP_METHOD_REINHARD + color = tonemapping_reinhard(color.rgb); +#else ifdef TONEMAP_METHOD_REINHARD_LUMINANCE + color = tonemapping_reinhard_luminance(color.rgb); +#else ifdef TONEMAP_METHOD_ACES_FITTED + color = ACESFitted(color.rgb); +#else ifdef TONEMAP_METHOD_AGX + color = applyAgXLog(color); + color = applyLUT3D(color, 32.0); +#else ifdef TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM + color = somewhat_boring_display_transform(color.rgb); +#else ifdef TONEMAP_METHOD_TONY_MC_MAPFACE + color = sample_tony_mc_mapface_lut(color); +#else ifdef TONEMAP_METHOD_BLENDER_FILMIC + color = sample_blender_filmic_lut(color.rgb); +#endif + + // Perceptual post tonemapping grading + color = saturation(color, view.color_grading.post_saturation); + + return vec4(color, in.a); +} + diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index 24f7d954584ea..be6ca62646426 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -26,6 +26,7 @@ debug_asset_server = ["bevy_asset/debug_asset_server"] detailed_trace = ["bevy_utils/detailed_trace"] # Image format support for texture loading (PNG and HDR are enabled by default) +exr = ["bevy_render/exr"] hdr = ["bevy_render/hdr"] png = ["bevy_render/png"] tga = ["bevy_render/tga"] @@ -38,6 +39,9 @@ ktx2 = ["bevy_render/ktx2"] zlib = ["bevy_render/zlib"] zstd = ["bevy_render/zstd"] +# Include tonemapping LUT KTX2 files. +tonemapping_luts = ["bevy_core_pipeline/tonemapping_luts"] + # Audio format support (vorbis is enabled by default) flac = ["bevy_audio/flac"] mp3 = ["bevy_audio/mp3"] diff --git a/crates/bevy_pbr/src/material.rs b/crates/bevy_pbr/src/material.rs index 82eaac06a8326..410b702c4933e 100644 --- a/crates/bevy_pbr/src/material.rs +++ b/crates/bevy_pbr/src/material.rs @@ -6,7 +6,7 @@ use bevy_app::{App, Plugin}; use bevy_asset::{AddAsset, AssetEvent, AssetServer, Assets, Handle}; use bevy_core_pipeline::{ core_3d::{AlphaMask3d, Opaque3d, Transparent3d}, - tonemapping::Tonemapping, + tonemapping::{DebandDither, Tonemapping}, }; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ @@ -367,6 +367,7 @@ pub fn queue_material_meshes( &ExtractedView, &VisibleEntities, Option<&Tonemapping>, + Option<&DebandDither>, Option<&EnvironmentMapLight>, &mut RenderPhase, &mut RenderPhase, @@ -379,6 +380,7 @@ pub fn queue_material_meshes( view, visible_entities, tonemapping, + dither, environment_map, mut opaque_phase, mut alpha_mask_phase, @@ -400,13 +402,26 @@ pub fn queue_material_meshes( view_key |= MeshPipelineKey::ENVIRONMENT_MAP; } - if let Some(Tonemapping::Enabled { deband_dither }) = tonemapping { - if !view.hdr { + if !view.hdr { + if let Some(tonemapping) = tonemapping { view_key |= MeshPipelineKey::TONEMAP_IN_SHADER; - - if *deband_dither { - view_key |= MeshPipelineKey::DEBAND_DITHER; - } + view_key |= match tonemapping { + Tonemapping::None => MeshPipelineKey::TONEMAP_METHOD_NONE, + Tonemapping::Reinhard => MeshPipelineKey::TONEMAP_METHOD_REINHARD, + Tonemapping::ReinhardLuminance => { + MeshPipelineKey::TONEMAP_METHOD_REINHARD_LUMINANCE + } + Tonemapping::AcesFitted => MeshPipelineKey::TONEMAP_METHOD_ACES_FITTED, + Tonemapping::AgX => MeshPipelineKey::TONEMAP_METHOD_AGX, + Tonemapping::SomewhatBoringDisplayTransform => { + MeshPipelineKey::TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM + } + Tonemapping::TonyMcMapface => MeshPipelineKey::TONEMAP_METHOD_TONY_MC_MAPFACE, + Tonemapping::BlenderFilmic => MeshPipelineKey::TONEMAP_METHOD_BLENDER_FILMIC, + }; + } + if let Some(DebandDither::Enabled) = dither { + view_key |= MeshPipelineKey::DEBAND_DITHER; } } diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index 440011ac4a0bf..ca77f917c849d 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -1150,6 +1150,7 @@ pub fn prepare_lights( view_projection: None, projection: cube_face_projection, hdr: false, + color_grading: Default::default(), }, RenderPhase::::default(), LightEntity::Point { @@ -1207,6 +1208,7 @@ pub fn prepare_lights( projection: spot_projection, view_projection: None, hdr: false, + color_grading: Default::default(), }, RenderPhase::::default(), LightEntity::Spot { light_entity }, @@ -1272,6 +1274,7 @@ pub fn prepare_lights( projection: cascade.projection, view_projection: Some(cascade.view_projection), hdr: false, + color_grading: Default::default(), }, RenderPhase::::default(), LightEntity::Directional { diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index 34350740d58cc..1adb5db10ca58 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -6,7 +6,12 @@ use crate::{ }; use bevy_app::Plugin; use bevy_asset::{load_internal_asset, Assets, Handle, HandleUntyped}; -use bevy_core_pipeline::prepass::ViewPrepassTextures; +use bevy_core_pipeline::{ + prepass::ViewPrepassTextures, + tonemapping::{ + get_lut_bind_group_layout_entries, get_lut_bindings, Tonemapping, TonemappingLuts, + }, +}; use bevy_ecs::{ prelude::*, query::ROQueryItem, @@ -418,10 +423,14 @@ impl FromWorld for MeshPipeline { environment_map::get_bind_group_layout_entries([11, 12, 13]); entries.extend_from_slice(&environment_map_entries); + // Tonemapping + let tonemapping_lut_entries = get_lut_bind_group_layout_entries([14, 15]); + entries.extend_from_slice(&tonemapping_lut_entries); + if cfg!(not(feature = "webgl")) { // Depth texture entries.push(BindGroupLayoutEntry { - binding: 14, + binding: 16, visibility: ShaderStages::FRAGMENT, ty: BindingType::Texture { multisampled, @@ -432,7 +441,7 @@ impl FromWorld for MeshPipeline { }); // Normal texture entries.push(BindGroupLayoutEntry { - binding: 15, + binding: 17, visibility: ShaderStages::FRAGMENT, ty: BindingType::Texture { multisampled, @@ -574,20 +583,29 @@ bitflags::bitflags! { // NOTE: Apparently quadro drivers support up to 64x MSAA. /// MSAA uses the highest 3 bits for the MSAA log2(sample count) to support up to 128x MSAA. pub struct MeshPipelineKey: u32 { - const NONE = 0; - const HDR = (1 << 0); - const TONEMAP_IN_SHADER = (1 << 1); - const DEBAND_DITHER = (1 << 2); - const DEPTH_PREPASS = (1 << 3); - const NORMAL_PREPASS = (1 << 4); - const ALPHA_MASK = (1 << 5); - const ENVIRONMENT_MAP = (1 << 6); - const BLEND_RESERVED_BITS = Self::BLEND_MASK_BITS << Self::BLEND_SHIFT_BITS; // ← Bitmask reserving bits for the blend state - const BLEND_OPAQUE = (0 << Self::BLEND_SHIFT_BITS); // ← Values are just sequential within the mask, and can range from 0 to 3 - const BLEND_PREMULTIPLIED_ALPHA = (1 << Self::BLEND_SHIFT_BITS); // - const BLEND_MULTIPLY = (2 << Self::BLEND_SHIFT_BITS); // ← We still have room for one more value without adding more bits - const MSAA_RESERVED_BITS = Self::MSAA_MASK_BITS << Self::MSAA_SHIFT_BITS; - const PRIMITIVE_TOPOLOGY_RESERVED_BITS = Self::PRIMITIVE_TOPOLOGY_MASK_BITS << Self::PRIMITIVE_TOPOLOGY_SHIFT_BITS; + const NONE = 0; + const HDR = (1 << 0); + const TONEMAP_IN_SHADER = (1 << 1); + const DEBAND_DITHER = (1 << 2); + const DEPTH_PREPASS = (1 << 3); + const NORMAL_PREPASS = (1 << 4); + const ALPHA_MASK = (1 << 5); + const ENVIRONMENT_MAP = (1 << 6); + const BLEND_RESERVED_BITS = Self::BLEND_MASK_BITS << Self::BLEND_SHIFT_BITS; // ← Bitmask reserving bits for the blend state + const BLEND_OPAQUE = (0 << Self::BLEND_SHIFT_BITS); // ← Values are just sequential within the mask, and can range from 0 to 3 + const BLEND_PREMULTIPLIED_ALPHA = (1 << Self::BLEND_SHIFT_BITS); // + const BLEND_MULTIPLY = (2 << Self::BLEND_SHIFT_BITS); // ← We still have room for one more value without adding more bits + const MSAA_RESERVED_BITS = Self::MSAA_MASK_BITS << Self::MSAA_SHIFT_BITS; + const PRIMITIVE_TOPOLOGY_RESERVED_BITS = Self::PRIMITIVE_TOPOLOGY_MASK_BITS << Self::PRIMITIVE_TOPOLOGY_SHIFT_BITS; + const TONEMAP_METHOD_RESERVED_BITS = Self::TONEMAP_METHOD_MASK_BITS << Self::TONEMAP_METHOD_SHIFT_BITS; + const TONEMAP_METHOD_NONE = 0 << Self::TONEMAP_METHOD_SHIFT_BITS; + const TONEMAP_METHOD_REINHARD = 1 << Self::TONEMAP_METHOD_SHIFT_BITS; + const TONEMAP_METHOD_REINHARD_LUMINANCE = 2 << Self::TONEMAP_METHOD_SHIFT_BITS; + const TONEMAP_METHOD_ACES_FITTED = 3 << Self::TONEMAP_METHOD_SHIFT_BITS; + const TONEMAP_METHOD_AGX = 4 << Self::TONEMAP_METHOD_SHIFT_BITS; + const TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM = 5 << Self::TONEMAP_METHOD_SHIFT_BITS; + const TONEMAP_METHOD_TONY_MC_MAPFACE = 6 << Self::TONEMAP_METHOD_SHIFT_BITS; + const TONEMAP_METHOD_BLENDER_FILMIC = 7 << Self::TONEMAP_METHOD_SHIFT_BITS; } } @@ -600,6 +618,9 @@ impl MeshPipelineKey { const BLEND_MASK_BITS: u32 = 0b11; const BLEND_SHIFT_BITS: u32 = Self::PRIMITIVE_TOPOLOGY_SHIFT_BITS - Self::BLEND_MASK_BITS.count_ones(); + const TONEMAP_METHOD_MASK_BITS: u32 = 0b111; + const TONEMAP_METHOD_SHIFT_BITS: u32 = + Self::BLEND_SHIFT_BITS - Self::TONEMAP_METHOD_MASK_BITS.count_ones(); pub fn from_msaa_samples(msaa_samples: u32) -> Self { let msaa_bits = @@ -743,6 +764,26 @@ impl SpecializedMeshPipeline for MeshPipeline { if key.contains(MeshPipelineKey::TONEMAP_IN_SHADER) { shader_defs.push("TONEMAP_IN_SHADER".into()); + let method = key.intersection(MeshPipelineKey::TONEMAP_METHOD_RESERVED_BITS); + + if method == MeshPipelineKey::TONEMAP_METHOD_NONE { + shader_defs.push("TONEMAP_METHOD_NONE".into()); + } else if method == MeshPipelineKey::TONEMAP_METHOD_REINHARD { + shader_defs.push("TONEMAP_METHOD_REINHARD".into()); + } else if method == MeshPipelineKey::TONEMAP_METHOD_REINHARD_LUMINANCE { + shader_defs.push("TONEMAP_METHOD_REINHARD_LUMINANCE".into()); + } else if method == MeshPipelineKey::TONEMAP_METHOD_ACES_FITTED { + shader_defs.push("TONEMAP_METHOD_ACES_FITTED ".into()); + } else if method == MeshPipelineKey::TONEMAP_METHOD_AGX { + shader_defs.push("TONEMAP_METHOD_AGX".into()); + } else if method == MeshPipelineKey::TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM { + shader_defs.push("TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM".into()); + } else if method == MeshPipelineKey::TONEMAP_METHOD_BLENDER_FILMIC { + shader_defs.push("TONEMAP_METHOD_BLENDER_FILMIC".into()); + } else if method == MeshPipelineKey::TONEMAP_METHOD_TONY_MC_MAPFACE { + shader_defs.push("TONEMAP_METHOD_TONY_MC_MAPFACE".into()); + } + // Debanding is tied to tonemapping in the shader, cannot run without it. if key.contains(MeshPipelineKey::DEBAND_DITHER) { shader_defs.push("DEBAND_DITHER".into()); @@ -919,6 +960,7 @@ pub fn queue_mesh_view_bind_groups( &ViewClusterBindings, Option<&ViewPrepassTextures>, Option<&EnvironmentMapLight>, + &Tonemapping, )>, images: Res>, mut fallback_images: FallbackImagesMsaa, @@ -926,6 +968,7 @@ pub fn queue_mesh_view_bind_groups( fallback_cubemap: Res, msaa: Res, globals_buffer: Res, + tonemapping_luts: Res, ) { if let ( Some(view_binding), @@ -946,6 +989,7 @@ pub fn queue_mesh_view_bind_groups( view_cluster_bindings, prepass_textures, environment_map, + tonemapping, ) in &views { let layout = if msaa.samples() > 1 { @@ -1013,6 +1057,10 @@ pub fn queue_mesh_view_bind_groups( ); entries.extend_from_slice(&env_map); + let tonemapping_luts = + get_lut_bindings(&images, &tonemapping_luts, tonemapping, [14, 15]); + entries.extend_from_slice(&tonemapping_luts); + // When using WebGL with MSAA, we can't create the fallback textures required by the prepass // When using WebGL, and MSAA is disabled, we can't bind the textures either if cfg!(not(feature = "webgl")) { @@ -1025,7 +1073,7 @@ pub fn queue_mesh_view_bind_groups( } }; entries.push(BindGroupEntry { - binding: 14, + binding: 16, resource: BindingResource::TextureView(depth_view), }); @@ -1038,7 +1086,7 @@ pub fn queue_mesh_view_bind_groups( } }; entries.push(BindGroupEntry { - binding: 15, + binding: 17, resource: BindingResource::TextureView(normal_view), }); } diff --git a/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl b/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl index cab2a406b156b..e0890ea3ea3a6 100644 --- a/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl +++ b/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl @@ -53,14 +53,19 @@ var environment_map_specular: texture_cube; @group(0) @binding(13) var environment_map_sampler: sampler; -#ifdef MULTISAMPLED @group(0) @binding(14) -var depth_prepass_texture: texture_depth_multisampled_2d; +var dt_lut_texture: texture_3d; @group(0) @binding(15) +var dt_lut_sampler: sampler; + +#ifdef MULTISAMPLED +@group(0) @binding(16) +var depth_prepass_texture: texture_depth_multisampled_2d; +@group(0) @binding(17) var normal_prepass_texture: texture_multisampled_2d; #else -@group(0) @binding(14) +@group(0) @binding(16) var depth_prepass_texture: texture_depth_2d; -@group(0) @binding(15) +@group(0) @binding(17) var normal_prepass_texture: texture_2d; #endif diff --git a/crates/bevy_pbr/src/render/pbr.wgsl b/crates/bevy_pbr/src/render/pbr.wgsl index 43e19fd7c32c5..b455d7bb6a520 100644 --- a/crates/bevy_pbr/src/render/pbr.wgsl +++ b/crates/bevy_pbr/src/render/pbr.wgsl @@ -107,11 +107,11 @@ fn fragment(in: FragmentInput) -> @location(0) vec4 { #endif #ifdef DEBAND_DITHER var output_rgb = output_color.rgb; - output_rgb = pow(output_rgb, vec3(1.0 / 2.2)); + output_rgb = powsafe(output_rgb, 1.0 / 2.2); output_rgb = output_rgb + screen_space_dither(in.frag_coord.xy); // This conversion back to linear space is required because our output texture format is // SRGB; the GPU will assume our output is linear and will apply an SRGB conversion. - output_rgb = pow(output_rgb, vec3(2.2)); + output_rgb = powsafe(output_rgb, 2.2); output_color = vec4(output_rgb, output_color.a); #endif #ifdef PREMULTIPLY_ALPHA diff --git a/crates/bevy_pbr/src/render/pbr_functions.wgsl b/crates/bevy_pbr/src/render/pbr_functions.wgsl index edeb194185413..852b84230d825 100644 --- a/crates/bevy_pbr/src/render/pbr_functions.wgsl +++ b/crates/bevy_pbr/src/render/pbr_functions.wgsl @@ -267,17 +267,6 @@ fn pbr( } #endif // NORMAL_PREPASS -#ifdef TONEMAP_IN_SHADER -fn tone_mapping(in: vec4) -> vec4 { - // tone_mapping - return vec4(reinhard_luminance(in.rgb), in.a); - - // Gamma correction. - // Not needed with sRGB buffer - // output_color.rgb = pow(output_color.rgb, vec3(1.0 / 2.2)); -} -#endif // TONEMAP_IN_SHADER - #ifdef DEBAND_DITHER fn dither(color: vec4, pos: vec2) -> vec4 { return vec4(color.rgb + screen_space_dither(pos.xy), color.a); diff --git a/crates/bevy_render/Cargo.toml b/crates/bevy_render/Cargo.toml index bff7571e26d65..b012049401e60 100644 --- a/crates/bevy_render/Cargo.toml +++ b/crates/bevy_render/Cargo.toml @@ -10,6 +10,7 @@ keywords = ["bevy"] [features] png = ["image/png"] +exr = ["image/exr"] hdr = ["image/hdr"] tga = ["image/tga"] jpeg = ["image/jpeg"] diff --git a/crates/bevy_render/src/camera/camera.rs b/crates/bevy_render/src/camera/camera.rs index 9434e5e80aaae..58fb2df3c878a 100644 --- a/crates/bevy_render/src/camera/camera.rs +++ b/crates/bevy_render/src/camera/camera.rs @@ -3,7 +3,7 @@ use crate::{ prelude::Image, render_asset::RenderAssets, render_resource::TextureView, - view::{ExtractedView, ExtractedWindows, VisibleEntities}, + view::{ColorGrading, ExtractedView, ExtractedWindows, VisibleEntities}, Extract, }; use bevy_asset::{AssetEvent, Assets, Handle}; @@ -530,12 +530,17 @@ pub fn extract_cameras( &CameraRenderGraph, &GlobalTransform, &VisibleEntities, + Option<&ColorGrading>, )>, >, primary_window: Extract>>, ) { let primary_window = primary_window.iter().next(); - for (entity, camera, camera_render_graph, transform, visible_entities) in query.iter() { + for (entity, camera, camera_render_graph, transform, visible_entities, color_grading) in + query.iter() + { + let color_grading = *color_grading.unwrap_or(&ColorGrading::default()); + if !camera.is_active { continue; } @@ -567,6 +572,7 @@ pub fn extract_cameras( viewport_size.x, viewport_size.y, ), + color_grading, }, visible_entities.clone(), )); diff --git a/crates/bevy_render/src/texture/exr_texture_loader.rs b/crates/bevy_render/src/texture/exr_texture_loader.rs new file mode 100644 index 0000000000000..6e1c18a805d0a --- /dev/null +++ b/crates/bevy_render/src/texture/exr_texture_loader.rs @@ -0,0 +1,56 @@ +use crate::texture::{Image, TextureFormatPixelInfo}; +use anyhow::Result; +use bevy_asset::{AssetLoader, LoadContext, LoadedAsset}; +use bevy_utils::BoxedFuture; +use image::ImageDecoder; +use wgpu::{Extent3d, TextureDimension, TextureFormat}; + +/// Loads EXR textures as Texture assets +#[derive(Clone, Default)] +pub struct ExrTextureLoader; + +impl AssetLoader for ExrTextureLoader { + fn load<'a>( + &'a self, + bytes: &'a [u8], + load_context: &'a mut LoadContext, + ) -> BoxedFuture<'a, Result<()>> { + Box::pin(async move { + let format = TextureFormat::Rgba32Float; + debug_assert_eq!( + format.pixel_size(), + 4 * 4, + "Format should have 32bit x 4 size" + ); + + let decoder = image::codecs::openexr::OpenExrDecoder::with_alpha_preference( + std::io::Cursor::new(bytes), + Some(true), + )?; + let (width, height) = decoder.dimensions(); + + let total_bytes = decoder.total_bytes() as usize; + + let mut buf = vec![0u8; total_bytes]; + decoder.read_image(buf.as_mut_slice())?; + + let texture = Image::new( + Extent3d { + width, + height, + depth_or_array_layers: 1, + }, + TextureDimension::D2, + buf, + format, + ); + + load_context.set_default_asset(LoadedAsset::new(texture)); + Ok(()) + }) + } + + fn extensions(&self) -> &[&str] { + &["exr"] + } +} diff --git a/crates/bevy_render/src/texture/image.rs b/crates/bevy_render/src/texture/image.rs index 00559bed361ec..07a2ad8e493b8 100644 --- a/crates/bevy_render/src/texture/image.rs +++ b/crates/bevy_render/src/texture/image.rs @@ -16,6 +16,7 @@ use bevy_derive::{Deref, DerefMut}; use bevy_ecs::system::{lifetimeless::SRes, Resource, SystemParamItem}; use bevy_math::Vec2; use bevy_reflect::{FromReflect, Reflect, TypeUuid}; + use std::hash::Hash; use thiserror::Error; use wgpu::{Extent3d, TextureDimension, TextureFormat, TextureViewDescriptor}; @@ -33,6 +34,7 @@ pub enum ImageFormat { Dds, Farbfeld, Gif, + OpenExr, Hdr, Ico, Jpeg, @@ -52,6 +54,7 @@ impl ImageFormat { "image/jpeg" => ImageFormat::Jpeg, "image/ktx2" => ImageFormat::Ktx2, "image/png" => ImageFormat::Png, + "image/x-exr" => ImageFormat::OpenExr, "image/x-targa" | "image/x-tga" => ImageFormat::Tga, _ => return None, }) @@ -65,6 +68,7 @@ impl ImageFormat { "dds" => ImageFormat::Dds, "ff" | "farbfeld" => ImageFormat::Farbfeld, "gif" => ImageFormat::Gif, + "exr" => ImageFormat::OpenExr, "hdr" => ImageFormat::Hdr, "ico" => ImageFormat::Ico, "jpg" | "jpeg" => ImageFormat::Jpeg, @@ -85,6 +89,7 @@ impl ImageFormat { ImageFormat::Dds => image::ImageFormat::Dds, ImageFormat::Farbfeld => image::ImageFormat::Farbfeld, ImageFormat::Gif => image::ImageFormat::Gif, + ImageFormat::OpenExr => image::ImageFormat::OpenExr, ImageFormat::Hdr => image::ImageFormat::Hdr, ImageFormat::Ico => image::ImageFormat::Ico, ImageFormat::Jpeg => image::ImageFormat::Jpeg, diff --git a/crates/bevy_render/src/texture/ktx2.rs b/crates/bevy_render/src/texture/ktx2.rs index 2dbcaa8f9490c..6202df95d5502 100644 --- a/crates/bevy_render/src/texture/ktx2.rs +++ b/crates/bevy_render/src/texture/ktx2.rs @@ -242,15 +242,16 @@ pub fn ktx2_buffer_to_image( let mut wgpu_data = vec![Vec::default(); (layer_count * face_count) as usize]; for (level, level_data) in levels.iter().enumerate() { - let (level_width, level_height) = ( + let (level_width, level_height, level_depth) = ( (width as usize >> level).max(1), (height as usize >> level).max(1), + (depth as usize >> level).max(1), ); let (num_blocks_x, num_blocks_y) = ( ((level_width + block_width_pixels - 1) / block_width_pixels).max(1), ((level_height + block_height_pixels - 1) / block_height_pixels).max(1), ); - let level_bytes = num_blocks_x * num_blocks_y * block_bytes; + let level_bytes = num_blocks_x * num_blocks_y * level_depth * block_bytes; let mut index = 0; for _layer in 0..layer_count { diff --git a/crates/bevy_render/src/texture/mod.rs b/crates/bevy_render/src/texture/mod.rs index de76edf8e2682..f9b32a1eb754b 100644 --- a/crates/bevy_render/src/texture/mod.rs +++ b/crates/bevy_render/src/texture/mod.rs @@ -2,6 +2,8 @@ mod basis; #[cfg(feature = "dds")] mod dds; +#[cfg(feature = "exr")] +mod exr_texture_loader; mod fallback_image; #[cfg(feature = "hdr")] mod hdr_texture_loader; @@ -19,6 +21,8 @@ pub use self::image::*; pub use self::ktx2::*; #[cfg(feature = "dds")] pub use dds::*; +#[cfg(feature = "exr")] +pub use exr_texture_loader::*; #[cfg(feature = "hdr")] pub use hdr_texture_loader::*; @@ -79,6 +83,11 @@ impl Plugin for ImagePlugin { app.init_asset_loader::(); } + #[cfg(feature = "exr")] + { + app.init_asset_loader::(); + } + #[cfg(feature = "hdr")] { app.init_asset_loader::(); diff --git a/crates/bevy_render/src/view/mod.rs b/crates/bevy_render/src/view/mod.rs index e1f9d9f7eee96..4f51ba9291135 100644 --- a/crates/bevy_render/src/view/mod.rs +++ b/crates/bevy_render/src/view/mod.rs @@ -106,6 +106,7 @@ pub struct ExtractedView { pub hdr: bool, // uvec4(origin.x, origin.y, width, height) pub viewport: UVec4, + pub color_grading: ColorGrading, } impl ExtractedView { @@ -115,6 +116,40 @@ impl ExtractedView { } } +/// Configures basic color grading parameters to adjust the image appearance. Grading is applied just before/after tonemapping for a given [`Camera`](crate::camera::Camera) entity. +#[derive(Component, Reflect, Debug, Copy, Clone, ShaderType)] +#[reflect(Component)] +pub struct ColorGrading { + /// Exposure value (EV) offset, measured in stops. + pub exposure: f32, + + /// Non-linear luminance adjustment applied before tonemapping. y = pow(x, gamma) + pub gamma: f32, + + /// Saturation adjustment applied before tonemapping. + /// Values below 1.0 desaturate, with a value of 0.0 resulting in a grayscale image + /// with luminance defined by ITU-R BT.709. + /// Values above 1.0 increase saturation. + pub pre_saturation: f32, + + /// Saturation adjustment applied after tonemapping. + /// Values below 1.0 desaturate, with a value of 0.0 resulting in a grayscale image + /// with luminance defined by ITU-R BT.709 + /// Values above 1.0 increase saturation. + pub post_saturation: f32, +} + +impl Default for ColorGrading { + fn default() -> Self { + Self { + exposure: 0.0, + gamma: 1.0, + pre_saturation: 1.0, + post_saturation: 1.0, + } + } +} + #[derive(Clone, ShaderType)] pub struct ViewUniform { view_proj: Mat4, @@ -126,6 +161,7 @@ pub struct ViewUniform { world_position: Vec3, // viewport(x_origin, y_origin, width, height) viewport: Vec4, + color_grading: ColorGrading, } #[derive(Resource, Default)] @@ -287,6 +323,7 @@ fn prepare_view_uniforms( inverse_projection, world_position: camera.transform.translation(), viewport: camera.viewport.as_vec4(), + color_grading: camera.color_grading, }), }; diff --git a/crates/bevy_render/src/view/view.wgsl b/crates/bevy_render/src/view/view.wgsl index 00a2fcbd5f931..8b4ca3fc71817 100644 --- a/crates/bevy_render/src/view/view.wgsl +++ b/crates/bevy_render/src/view/view.wgsl @@ -1,5 +1,12 @@ #define_import_path bevy_render::view +struct ColorGrading { + exposure: f32, + gamma: f32, + pre_saturation: f32, + post_saturation: f32, +} + struct View { view_proj: mat4x4, inverse_view_proj: mat4x4, @@ -10,4 +17,5 @@ struct View { world_position: vec3, // viewport(x_origin, y_origin, width, height) viewport: vec4, + color_grading: ColorGrading, }; diff --git a/crates/bevy_sprite/src/mesh2d/color_material.wgsl b/crates/bevy_sprite/src/mesh2d/color_material.wgsl index a78eedbbb96d5..28d16f148d5ad 100644 --- a/crates/bevy_sprite/src/mesh2d/color_material.wgsl +++ b/crates/bevy_sprite/src/mesh2d/color_material.wgsl @@ -1,6 +1,10 @@ #import bevy_sprite::mesh2d_types #import bevy_sprite::mesh2d_view_bindings +#ifdef TONEMAP_IN_SHADER +#import bevy_core_pipeline::tonemapping +#endif + struct ColorMaterial { color: vec4, // 'flags' is a bit field indicating various options. u32 is 32 bits so we have up to 32 options. @@ -31,5 +35,8 @@ fn fragment(in: FragmentInput) -> @location(0) vec4 { if ((material.flags & COLOR_MATERIAL_FLAGS_TEXTURE_BIT) != 0u) { output_color = output_color * textureSample(texture, texture_sampler, in.uv); } +#ifdef TONEMAP_IN_SHADER + output_color = tone_mapping(output_color); +#endif return output_color; } diff --git a/crates/bevy_sprite/src/mesh2d/material.rs b/crates/bevy_sprite/src/mesh2d/material.rs index 1ae7252a17884..29dd21715e40d 100644 --- a/crates/bevy_sprite/src/mesh2d/material.rs +++ b/crates/bevy_sprite/src/mesh2d/material.rs @@ -1,6 +1,9 @@ use bevy_app::{App, Plugin}; use bevy_asset::{AddAsset, AssetEvent, AssetServer, Assets, Handle}; -use bevy_core_pipeline::{core_2d::Transparent2d, tonemapping::Tonemapping}; +use bevy_core_pipeline::{ + core_2d::Transparent2d, + tonemapping::{DebandDither, Tonemapping}, +}; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ prelude::*, @@ -327,6 +330,7 @@ pub fn queue_material2d_meshes( &ExtractedView, &VisibleEntities, Option<&Tonemapping>, + Option<&DebandDither>, &mut RenderPhase, )>, ) where @@ -336,19 +340,32 @@ pub fn queue_material2d_meshes( return; } - for (view, visible_entities, tonemapping, mut transparent_phase) in &mut views { + for (view, visible_entities, tonemapping, dither, mut transparent_phase) in &mut views { let draw_transparent_pbr = transparent_draw_functions.read().id::>(); let mut view_key = Mesh2dPipelineKey::from_msaa_samples(msaa.samples()) | Mesh2dPipelineKey::from_hdr(view.hdr); - if let Some(Tonemapping::Enabled { deband_dither }) = tonemapping { - if !view.hdr { + if !view.hdr { + if let Some(tonemapping) = tonemapping { view_key |= Mesh2dPipelineKey::TONEMAP_IN_SHADER; - - if *deband_dither { - view_key |= Mesh2dPipelineKey::DEBAND_DITHER; - } + view_key |= match tonemapping { + Tonemapping::None => Mesh2dPipelineKey::TONEMAP_METHOD_NONE, + Tonemapping::Reinhard => Mesh2dPipelineKey::TONEMAP_METHOD_REINHARD, + Tonemapping::ReinhardLuminance => { + Mesh2dPipelineKey::TONEMAP_METHOD_REINHARD_LUMINANCE + } + Tonemapping::AcesFitted => Mesh2dPipelineKey::TONEMAP_METHOD_ACES_FITTED, + Tonemapping::AgX => Mesh2dPipelineKey::TONEMAP_METHOD_AGX, + Tonemapping::SomewhatBoringDisplayTransform => { + Mesh2dPipelineKey::TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM + } + Tonemapping::TonyMcMapface => Mesh2dPipelineKey::TONEMAP_METHOD_TONY_MC_MAPFACE, + Tonemapping::BlenderFilmic => Mesh2dPipelineKey::TONEMAP_METHOD_BLENDER_FILMIC, + }; + } + if let Some(DebandDither::Enabled) = dither { + view_key |= Mesh2dPipelineKey::DEBAND_DITHER; } } diff --git a/crates/bevy_sprite/src/mesh2d/mesh.rs b/crates/bevy_sprite/src/mesh2d/mesh.rs index c46ef1d883669..5405195db0965 100644 --- a/crates/bevy_sprite/src/mesh2d/mesh.rs +++ b/crates/bevy_sprite/src/mesh2d/mesh.rs @@ -1,5 +1,6 @@ use bevy_app::Plugin; use bevy_asset::{load_internal_asset, Handle, HandleUntyped}; + use bevy_ecs::{ prelude::*, query::ROQueryItem, @@ -286,12 +287,21 @@ bitflags::bitflags! { // MSAA uses the highest 3 bits for the MSAA log2(sample count) to support up to 128x MSAA. // FIXME: make normals optional? pub struct Mesh2dPipelineKey: u32 { - const NONE = 0; - const HDR = (1 << 0); - const TONEMAP_IN_SHADER = (1 << 1); - const DEBAND_DITHER = (1 << 2); - const MSAA_RESERVED_BITS = Self::MSAA_MASK_BITS << Self::MSAA_SHIFT_BITS; - const PRIMITIVE_TOPOLOGY_RESERVED_BITS = Self::PRIMITIVE_TOPOLOGY_MASK_BITS << Self::PRIMITIVE_TOPOLOGY_SHIFT_BITS; + const NONE = 0; + const HDR = (1 << 0); + const TONEMAP_IN_SHADER = (1 << 1); + const DEBAND_DITHER = (1 << 2); + const MSAA_RESERVED_BITS = Self::MSAA_MASK_BITS << Self::MSAA_SHIFT_BITS; + const PRIMITIVE_TOPOLOGY_RESERVED_BITS = Self::PRIMITIVE_TOPOLOGY_MASK_BITS << Self::PRIMITIVE_TOPOLOGY_SHIFT_BITS; + const TONEMAP_METHOD_RESERVED_BITS = Self::TONEMAP_METHOD_MASK_BITS << Self::TONEMAP_METHOD_SHIFT_BITS; + const TONEMAP_METHOD_NONE = 0 << Self::TONEMAP_METHOD_SHIFT_BITS; + const TONEMAP_METHOD_REINHARD = 1 << Self::TONEMAP_METHOD_SHIFT_BITS; + const TONEMAP_METHOD_REINHARD_LUMINANCE = 2 << Self::TONEMAP_METHOD_SHIFT_BITS; + const TONEMAP_METHOD_ACES_FITTED = 3 << Self::TONEMAP_METHOD_SHIFT_BITS; + const TONEMAP_METHOD_AGX = 4 << Self::TONEMAP_METHOD_SHIFT_BITS; + const TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM = 5 << Self::TONEMAP_METHOD_SHIFT_BITS; + const TONEMAP_METHOD_TONY_MC_MAPFACE = 6 << Self::TONEMAP_METHOD_SHIFT_BITS; + const TONEMAP_METHOD_BLENDER_FILMIC = 7 << Self::TONEMAP_METHOD_SHIFT_BITS; } } @@ -300,6 +310,9 @@ impl Mesh2dPipelineKey { const MSAA_SHIFT_BITS: u32 = 32 - Self::MSAA_MASK_BITS.count_ones(); const PRIMITIVE_TOPOLOGY_MASK_BITS: u32 = 0b111; const PRIMITIVE_TOPOLOGY_SHIFT_BITS: u32 = Self::MSAA_SHIFT_BITS - 3; + const TONEMAP_METHOD_MASK_BITS: u32 = 0b111; + const TONEMAP_METHOD_SHIFT_BITS: u32 = + Self::PRIMITIVE_TOPOLOGY_SHIFT_BITS - Self::TONEMAP_METHOD_MASK_BITS.count_ones(); pub fn from_msaa_samples(msaa_samples: u32) -> Self { let msaa_bits = @@ -379,6 +392,27 @@ impl SpecializedMeshPipeline for Mesh2dPipeline { if key.contains(Mesh2dPipelineKey::TONEMAP_IN_SHADER) { shader_defs.push("TONEMAP_IN_SHADER".into()); + let method = key.intersection(Mesh2dPipelineKey::TONEMAP_METHOD_RESERVED_BITS); + + if method == Mesh2dPipelineKey::TONEMAP_METHOD_NONE { + shader_defs.push("TONEMAP_METHOD_NONE".into()); + } else if method == Mesh2dPipelineKey::TONEMAP_METHOD_REINHARD { + shader_defs.push("TONEMAP_METHOD_REINHARD".into()); + } else if method == Mesh2dPipelineKey::TONEMAP_METHOD_REINHARD_LUMINANCE { + shader_defs.push("TONEMAP_METHOD_REINHARD_LUMINANCE".into()); + } else if method == Mesh2dPipelineKey::TONEMAP_METHOD_ACES_FITTED { + shader_defs.push("TONEMAP_METHOD_ACES_FITTED".into()); + } else if method == Mesh2dPipelineKey::TONEMAP_METHOD_AGX { + shader_defs.push("TONEMAP_METHOD_AGX".into()); + } else if method == Mesh2dPipelineKey::TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM + { + shader_defs.push("TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM".into()); + } else if method == Mesh2dPipelineKey::TONEMAP_METHOD_BLENDER_FILMIC { + shader_defs.push("TONEMAP_METHOD_BLENDER_FILMIC".into()); + } else if method == Mesh2dPipelineKey::TONEMAP_METHOD_TONY_MC_MAPFACE { + shader_defs.push("TONEMAP_METHOD_TONY_MC_MAPFACE".into()); + } + // Debanding is tied to tonemapping in the shader, cannot run without it. if key.contains(Mesh2dPipelineKey::DEBAND_DITHER) { shader_defs.push("DEBAND_DITHER".into()); diff --git a/crates/bevy_sprite/src/mesh2d/mesh2d.wgsl b/crates/bevy_sprite/src/mesh2d/mesh2d.wgsl index 0be59aef75830..b2888bb055990 100644 --- a/crates/bevy_sprite/src/mesh2d/mesh2d.wgsl +++ b/crates/bevy_sprite/src/mesh2d/mesh2d.wgsl @@ -4,6 +4,10 @@ // NOTE: Bindings must come before functions that use them! #import bevy_sprite::mesh2d_functions +#ifdef TONEMAP_IN_SHADER +#import bevy_core_pipeline::tonemapping +#endif + struct Vertex { #ifdef VERTEX_POSITIONS @location(0) position: vec3, @@ -61,7 +65,11 @@ struct FragmentInput { @fragment fn fragment(in: FragmentInput) -> @location(0) vec4 { #ifdef VERTEX_COLORS - return in.color; + var color = in.color; +#ifdef TONEMAP_IN_SHADER + color = tone_mapping(color); +#endif + return color; #else return vec4(1.0, 0.0, 1.0, 1.0); #endif diff --git a/crates/bevy_sprite/src/render/mod.rs b/crates/bevy_sprite/src/render/mod.rs index 301ab2376c552..d6ee43be48658 100644 --- a/crates/bevy_sprite/src/render/mod.rs +++ b/crates/bevy_sprite/src/render/mod.rs @@ -5,7 +5,10 @@ use crate::{ Sprite, SPRITE_SHADER_HANDLE, }; use bevy_asset::{AssetEvent, Assets, Handle, HandleId}; -use bevy_core_pipeline::{core_2d::Transparent2d, tonemapping::Tonemapping}; +use bevy_core_pipeline::{ + core_2d::Transparent2d, + tonemapping::{DebandDither, Tonemapping}, +}; use bevy_ecs::{ prelude::*, system::{lifetimeless::*, SystemParamItem, SystemState}, @@ -147,18 +150,30 @@ bitflags::bitflags! { // NOTE: Apparently quadro drivers support up to 64x MSAA. // MSAA uses the highest 3 bits for the MSAA log2(sample count) to support up to 128x MSAA. pub struct SpritePipelineKey: u32 { - const NONE = 0; - const COLORED = (1 << 0); - const HDR = (1 << 1); - const TONEMAP_IN_SHADER = (1 << 2); - const DEBAND_DITHER = (1 << 3); - const MSAA_RESERVED_BITS = Self::MSAA_MASK_BITS << Self::MSAA_SHIFT_BITS; + const NONE = 0; + const COLORED = (1 << 0); + const HDR = (1 << 1); + const TONEMAP_IN_SHADER = (1 << 2); + const DEBAND_DITHER = (1 << 3); + const MSAA_RESERVED_BITS = Self::MSAA_MASK_BITS << Self::MSAA_SHIFT_BITS; + const TONEMAP_METHOD_RESERVED_BITS = Self::TONEMAP_METHOD_MASK_BITS << Self::TONEMAP_METHOD_SHIFT_BITS; + const TONEMAP_METHOD_NONE = 0 << Self::TONEMAP_METHOD_SHIFT_BITS; + const TONEMAP_METHOD_REINHARD = 1 << Self::TONEMAP_METHOD_SHIFT_BITS; + const TONEMAP_METHOD_REINHARD_LUMINANCE = 2 << Self::TONEMAP_METHOD_SHIFT_BITS; + const TONEMAP_METHOD_ACES_FITTED = 3 << Self::TONEMAP_METHOD_SHIFT_BITS; + const TONEMAP_METHOD_AGX = 4 << Self::TONEMAP_METHOD_SHIFT_BITS; + const TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM = 5 << Self::TONEMAP_METHOD_SHIFT_BITS; + const TONEMAP_METHOD_TONY_MC_MAPFACE = 6 << Self::TONEMAP_METHOD_SHIFT_BITS; + const TONEMAP_METHOD_BLENDER_FILMIC = 7 << Self::TONEMAP_METHOD_SHIFT_BITS; } } impl SpritePipelineKey { const MSAA_MASK_BITS: u32 = 0b111; const MSAA_SHIFT_BITS: u32 = 32 - Self::MSAA_MASK_BITS.count_ones(); + const TONEMAP_METHOD_MASK_BITS: u32 = 0b111; + const TONEMAP_METHOD_SHIFT_BITS: u32 = + Self::MSAA_SHIFT_BITS - Self::TONEMAP_METHOD_MASK_BITS.count_ones(); #[inline] pub const fn from_msaa_samples(msaa_samples: u32) -> Self { @@ -218,6 +233,27 @@ impl SpecializedRenderPipeline for SpritePipeline { if key.contains(SpritePipelineKey::TONEMAP_IN_SHADER) { shader_defs.push("TONEMAP_IN_SHADER".into()); + let method = key.intersection(SpritePipelineKey::TONEMAP_METHOD_RESERVED_BITS); + + if method == SpritePipelineKey::TONEMAP_METHOD_NONE { + shader_defs.push("TONEMAP_METHOD_NONE".into()); + } else if method == SpritePipelineKey::TONEMAP_METHOD_REINHARD { + shader_defs.push("TONEMAP_METHOD_REINHARD".into()); + } else if method == SpritePipelineKey::TONEMAP_METHOD_REINHARD_LUMINANCE { + shader_defs.push("TONEMAP_METHOD_REINHARD_LUMINANCE".into()); + } else if method == SpritePipelineKey::TONEMAP_METHOD_ACES_FITTED { + shader_defs.push("TONEMAP_METHOD_ACES_FITTED".into()); + } else if method == SpritePipelineKey::TONEMAP_METHOD_AGX { + shader_defs.push("TONEMAP_METHOD_AGX".into()); + } else if method == SpritePipelineKey::TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM + { + shader_defs.push("TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM".into()); + } else if method == SpritePipelineKey::TONEMAP_METHOD_BLENDER_FILMIC { + shader_defs.push("TONEMAP_METHOD_BLENDER_FILMIC".into()); + } else if method == SpritePipelineKey::TONEMAP_METHOD_TONY_MC_MAPFACE { + shader_defs.push("TONEMAP_METHOD_TONY_MC_MAPFACE".into()); + } + // Debanding is tied to tonemapping in the shader, cannot run without it. if key.contains(SpritePipelineKey::DEBAND_DITHER) { shader_defs.push("DEBAND_DITHER".into()); @@ -462,6 +498,7 @@ pub fn queue_sprites( &VisibleEntities, &ExtractedView, Option<&Tonemapping>, + Option<&DebandDither>, )>, events: Res, ) { @@ -517,17 +554,36 @@ pub fn queue_sprites( }); let image_bind_groups = &mut *image_bind_groups; - for (mut transparent_phase, visible_entities, view, tonemapping) in &mut views { + for (mut transparent_phase, visible_entities, view, tonemapping, dither) in &mut views { let mut view_key = SpritePipelineKey::from_hdr(view.hdr) | msaa_key; - if let Some(Tonemapping::Enabled { deband_dither }) = tonemapping { - if !view.hdr { - view_key |= SpritePipelineKey::TONEMAP_IN_SHADER; - if *deband_dither { - view_key |= SpritePipelineKey::DEBAND_DITHER; - } + if !view.hdr { + if let Some(tonemapping) = tonemapping { + view_key |= SpritePipelineKey::TONEMAP_IN_SHADER; + view_key |= match tonemapping { + Tonemapping::None => SpritePipelineKey::TONEMAP_METHOD_NONE, + Tonemapping::Reinhard => SpritePipelineKey::TONEMAP_METHOD_REINHARD, + Tonemapping::ReinhardLuminance => { + SpritePipelineKey::TONEMAP_METHOD_REINHARD_LUMINANCE + } + Tonemapping::AcesFitted => SpritePipelineKey::TONEMAP_METHOD_ACES_FITTED, + Tonemapping::AgX => SpritePipelineKey::TONEMAP_METHOD_AGX, + Tonemapping::SomewhatBoringDisplayTransform => { + SpritePipelineKey::TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM + } + Tonemapping::TonyMcMapface => { + SpritePipelineKey::TONEMAP_METHOD_TONY_MC_MAPFACE + } + Tonemapping::BlenderFilmic => { + SpritePipelineKey::TONEMAP_METHOD_BLENDER_FILMIC + } + }; + } + if let Some(DebandDither::Enabled) = dither { + view_key |= SpritePipelineKey::DEBAND_DITHER; } } + let pipeline = pipelines.specialize( &pipeline_cache, &sprite_pipeline, diff --git a/crates/bevy_sprite/src/render/sprite.wgsl b/crates/bevy_sprite/src/render/sprite.wgsl index 0bb90e61507e7..99d340286d737 100644 --- a/crates/bevy_sprite/src/render/sprite.wgsl +++ b/crates/bevy_sprite/src/render/sprite.wgsl @@ -45,7 +45,7 @@ fn fragment(in: VertexOutput) -> @location(0) vec4 { #endif #ifdef TONEMAP_IN_SHADER - color = vec4(reinhard_luminance(color.rgb), color.a); + color = tone_mapping(color); #endif return color; diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index 41ba6089816da..f213118a01093 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -277,6 +277,7 @@ pub fn extract_default_ui_camera_view( physical_size.x, physical_size.y, ), + color_grading: Default::default(), }) .id(); commands.get_or_spawn(entity).insert(( diff --git a/examples/3d/tonemapping.rs b/examples/3d/tonemapping.rs new file mode 100644 index 0000000000000..cc4f2cc3f7951 --- /dev/null +++ b/examples/3d/tonemapping.rs @@ -0,0 +1,697 @@ +//! This examples compares Tonemapping options + +use bevy::{ + core_pipeline::tonemapping::Tonemapping, + math::vec2, + pbr::CascadeShadowConfigBuilder, + prelude::*, + reflect::TypeUuid, + render::{ + render_resource::{ + AsBindGroup, Extent3d, SamplerDescriptor, ShaderRef, TextureDimension, TextureFormat, + }, + texture::ImageSampler, + view::ColorGrading, + }, + utils::HashMap, +}; +use std::f32::consts::PI; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_plugin(MaterialPlugin::::default()) + .insert_resource(CameraTransform( + Transform::from_xyz(0.7, 0.7, 1.0).looking_at(Vec3::new(0.0, 0.3, 0.0), Vec3::Y), + )) + .init_resource::() + .insert_resource(CurrentScene(1)) + .insert_resource(SelectedParameter { value: 0, max: 4 }) + .add_startup_system(setup) + .add_startup_system(setup_basic_scene) + .add_startup_system(setup_color_gradient_scene) + .add_startup_system(setup_image_viewer_scene) + .add_system(update_image_viewer) + .add_system(toggle_scene) + .add_system(toggle_tonemapping_method) + .add_system(update_color_grading_settings) + .add_system(update_ui) + .run(); +} + +fn setup( + mut commands: Commands, + asset_server: Res, + camera_transform: Res, +) { + // camera + commands.spawn(( + Camera3dBundle { + camera: Camera { + hdr: true, + ..default() + }, + transform: camera_transform.0, + ..default() + }, + EnvironmentMapLight { + diffuse_map: asset_server.load("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2"), + specular_map: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"), + }, + )); + + // ui + commands.spawn( + TextBundle::from_section( + "", + TextStyle { + font: asset_server.load("fonts/FiraMono-Medium.ttf"), + font_size: 18.0, + color: Color::WHITE, + }, + ) + .with_style(Style { + position_type: PositionType::Absolute, + position: UiRect { + top: Val::Px(10.0), + left: Val::Px(10.0), + ..default() + }, + ..default() + }), + ); +} + +fn setup_basic_scene( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, + mut images: ResMut>, + asset_server: Res, +) { + // plane + commands.spawn(( + PbrBundle { + mesh: meshes.add(Mesh::from(shape::Plane { + size: 5.0, + ..default() + })), + material: materials.add(StandardMaterial { + base_color: Color::rgb(0.3, 0.5, 0.3), + perceptual_roughness: 0.5, + ..default() + }), + ..default() + }, + SceneNumber(1), + )); + + // cubes + let cube_material = materials.add(StandardMaterial { + base_color_texture: Some(images.add(uv_debug_texture())), + ..default() + }); + + let cube_mesh = meshes.add(Mesh::from(shape::Cube { size: 0.25 })); + for i in 0..5 { + commands.spawn(( + PbrBundle { + mesh: cube_mesh.clone(), + material: cube_material.clone(), + transform: Transform::from_xyz(i as f32 * 0.25 - 1.0, 0.125, -i as f32 * 0.5), + ..default() + }, + SceneNumber(1), + )); + } + + // spheres + for i in 0..6 { + let j = i % 3; + let s_val = if i < 3 { 0.0 } else { 0.2 }; + let material = if j == 0 { + materials.add(StandardMaterial { + base_color: Color::rgb(s_val, s_val, 1.0), + perceptual_roughness: 0.089, + metallic: 0.0, + ..default() + }) + } else if j == 1 { + materials.add(StandardMaterial { + base_color: Color::rgb(s_val, 1.0, s_val), + perceptual_roughness: 0.089, + metallic: 0.0, + ..default() + }) + } else { + materials.add(StandardMaterial { + base_color: Color::rgb(1.0, s_val, s_val), + perceptual_roughness: 0.089, + metallic: 0.0, + ..default() + }) + }; + commands.spawn(( + PbrBundle { + mesh: meshes.add(Mesh::from(shape::UVSphere { + radius: 0.125, + sectors: 128, + stacks: 128, + })), + material, + transform: Transform::from_xyz( + j as f32 * 0.25 + if i < 3 { -0.15 } else { 0.15 } - 0.4, + 0.125, + -j as f32 * 0.25 + if i < 3 { -0.15 } else { 0.15 } + 0.4, + ), + ..default() + }, + SceneNumber(1), + )); + } + + // Flight Helmet + commands.spawn(( + SceneBundle { + scene: asset_server.load("models/FlightHelmet/FlightHelmet.gltf#Scene0"), + transform: Transform::from_xyz(0.5, 0.0, -0.5) + .with_rotation(Quat::from_rotation_y(-0.15 * PI)), + ..default() + }, + SceneNumber(1), + )); + + // light + commands.spawn(( + DirectionalLightBundle { + directional_light: DirectionalLight { + shadows_enabled: true, + illuminance: 50000.0, + ..default() + }, + transform: Transform::from_rotation(Quat::from_euler( + EulerRot::ZYX, + 0.0, + PI * -0.15, + PI * -0.15, + )), + cascade_shadow_config: CascadeShadowConfigBuilder { + maximum_distance: 3.0, + first_cascade_far_bound: 0.9, + ..default() + } + .into(), + ..default() + }, + SceneNumber(1), + )); +} + +fn setup_color_gradient_scene( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, + camera_transform: Res, +) { + let mut transform = camera_transform.0; + transform.translation += transform.forward(); + + commands.spawn(( + MaterialMeshBundle { + mesh: meshes.add(Mesh::from(shape::Quad { + size: vec2(1.0, 1.0) * 0.7, + flip: false, + })), + material: materials.add(ColorGradientMaterial {}), + transform, + visibility: Visibility::Hidden, + ..default() + }, + SceneNumber(2), + )); +} + +fn setup_image_viewer_scene( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, + camera_transform: Res, + asset_server: Res, +) { + let mut transform = camera_transform.0; + transform.translation += transform.forward(); + + // exr/hdr viewer (exr requires enabling bevy feature) + commands.spawn(( + PbrBundle { + mesh: meshes.add(Mesh::from(shape::Quad { + size: vec2(1.0, 1.0), + flip: false, + })), + material: materials.add(StandardMaterial { + base_color_texture: None, + unlit: true, + ..default() + }), + transform, + visibility: Visibility::Hidden, + ..default() + }, + SceneNumber(3), + HDRViewer, + )); + + commands + .spawn(( + TextBundle::from_section( + "Drag and drop an HDR or EXR file", + TextStyle { + font: asset_server.load("fonts/FiraMono-Medium.ttf"), + font_size: 36.0, + color: Color::BLACK, + }, + ) + .with_text_alignment(TextAlignment::Center) + .with_style(Style { + align_self: AlignSelf::Center, + margin: UiRect::all(Val::Auto), + ..default() + }), + SceneNumber(3), + )) + .insert(Visibility::Hidden); +} + +// ---------------------------------------------------------------------------- + +#[allow(clippy::too_many_arguments)] +fn update_image_viewer( + image_mesh: Query<(&Handle, &Handle), With>, + text: Query, With)>, + mut materials: ResMut>, + mut meshes: ResMut>, + images: Res>, + mut drop_events: EventReader, + mut drop_hovered: Local, + asset_server: Res, + mut image_events: EventReader>, + mut commands: Commands, +) { + let mut new_image: Option> = None; + + for event in drop_events.iter() { + match event { + FileDragAndDrop::DroppedFile { path_buf, .. } => { + new_image = Some(asset_server.load(path_buf.to_string_lossy().to_string())); + *drop_hovered = false; + } + FileDragAndDrop::HoveredFile { .. } => *drop_hovered = true, + FileDragAndDrop::HoveredFileCancelled { .. } => *drop_hovered = false, + } + } + + for (mat_h, mesh_h) in &image_mesh { + if let Some(mat) = materials.get_mut(mat_h) { + if let Some(ref new_image) = new_image { + mat.base_color_texture = Some(new_image.clone()); + + if let Ok(text_entity) = text.get_single() { + commands.entity(text_entity).despawn(); + } + } + + for event in image_events.iter() { + let image_changed_h = match event { + AssetEvent::Created { handle } | AssetEvent::Modified { handle } => handle, + _ => continue, + }; + if let Some(base_color_texture) = mat.base_color_texture.clone() { + if image_changed_h == &base_color_texture { + if let Some(image_changed) = images.get(image_changed_h) { + let size = image_changed.size().normalize_or_zero() * 1.4; + // Resize Mesh + let quad = Mesh::from(shape::Quad::new(size)); + let _ = meshes.set(mesh_h, quad); + } + } + } + } + } + } +} + +fn toggle_scene( + keys: Res>, + mut query: Query<(&mut Visibility, &SceneNumber)>, + mut current_scene: ResMut, +) { + let mut pressed = None; + if keys.just_pressed(KeyCode::Q) { + pressed = Some(1); + } else if keys.just_pressed(KeyCode::W) { + pressed = Some(2); + } else if keys.just_pressed(KeyCode::E) { + pressed = Some(3); + } + + if let Some(pressed) = pressed { + current_scene.0 = pressed; + + for (mut visibility, scene) in query.iter_mut() { + if scene.0 == pressed { + *visibility = Visibility::Visible; + } else { + *visibility = Visibility::Hidden; + } + } + } +} + +fn toggle_tonemapping_method( + keys: Res>, + mut tonemapping: Query<&mut Tonemapping>, + mut color_grading: Query<&mut ColorGrading>, + per_method_settings: Res, +) { + let mut method = tonemapping.single_mut(); + let mut color_grading = color_grading.single_mut(); + + if keys.just_pressed(KeyCode::Key1) { + *method = Tonemapping::None; + } else if keys.just_pressed(KeyCode::Key2) { + *method = Tonemapping::Reinhard; + } else if keys.just_pressed(KeyCode::Key3) { + *method = Tonemapping::ReinhardLuminance; + } else if keys.just_pressed(KeyCode::Key4) { + *method = Tonemapping::AcesFitted; + } else if keys.just_pressed(KeyCode::Key5) { + *method = Tonemapping::AgX; + } else if keys.just_pressed(KeyCode::Key6) { + *method = Tonemapping::SomewhatBoringDisplayTransform; + } else if keys.just_pressed(KeyCode::Key7) { + *method = Tonemapping::TonyMcMapface; + } else if keys.just_pressed(KeyCode::Key8) { + *method = Tonemapping::BlenderFilmic; + } + + *color_grading = *per_method_settings.settings.get(&method).unwrap(); +} + +#[derive(Resource)] +struct SelectedParameter { + value: i32, + max: i32, +} + +impl SelectedParameter { + fn next(&mut self) { + self.value = (self.value + 1).rem_euclid(self.max); + } + fn prev(&mut self) { + self.value = (self.value - 1).rem_euclid(self.max); + } +} + +fn update_color_grading_settings( + keys: Res>, + time: Res