Initial tonemapping options (#7594)

# Objective

Splits tone mapping from https://github.com/bevyengine/bevy/pull/6677 into a separate PR.
Address https://github.com/bevyengine/bevy/issues/2264.
Adds tone mapping options:
- None: Bypasses tonemapping for instances where users want colors output to match those set.
- Reinhard
- Reinhard Luminance: Bevy's exiting tonemapping
- [ACES](https://github.com/TheRealMJP/BakingLab/blob/master/BakingLab/ACES.hlsl) (Fitted version, based on the same implementation that Godot 4 uses) see https://github.com/bevyengine/bevy/issues/2264
- [AgX](https://github.com/sobotka/AgX)
- SomewhatBoringDisplayTransform
- TonyMcMapface
- Blender Filmic

This PR also adds support for EXR images so they can be used to compare tonemapping options with reference images.

## Migration Guide
- Tonemapping is now an enum with NONE and the various tonemappers.
- The DebandDither is now a separate component.




Co-authored-by: JMS55 <47158642+JMS55@users.noreply.github.com>
This commit is contained in:
Griffin 2023-02-19 20:38:13 +00:00
parent d46427b4e4
commit 912fb58869
37 changed files with 1849 additions and 154 deletions

View file

@ -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"

View file

@ -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<f32>,
#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<f32>) -> vec3<f32> {
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<f32>) -> vec3<f32> {
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<f32>) -> vec3<f32> {
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<f32> {
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;
}

View file

@ -15,6 +15,7 @@ keywords = ["bevy"]
[features]
trace = []
webgl = []
tonemapping_luts = []
[dependencies]
# bevy

View file

@ -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,
}
}
}

View file

@ -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(),
}
}
}

View file

@ -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

View file

@ -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<Image>,
agx: Handle<Image>,
tony_mc_mapface: Handle<Image>,
}
pub struct TonemappingPlugin;
impl Plugin for TonemappingPlugin {
@ -36,9 +48,47 @@ impl Plugin for TonemappingPlugin {
Shader::from_wgsl
);
if !app.world.is_resource_added::<TonemappingLuts>() {
let mut images = app.world.resource_mut::<Assets<Image>>();
#[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::<TonemappingLuts>::default());
app.register_type::<Tonemapping>();
app.register_type::<DebandDither>();
app.add_plugin(ExtractComponentPlugin::<Tonemapping>::default());
app.add_plugin(ExtractComponentPlugin::<DebandDither>::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<Camera>)]
#[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.
/// <https://github.com/TheRealMJP/BakingLab/blob/master/BakingLab/ACES.hlsl>
/// 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
/// <https://github.com/sobotka/AgX>
/// 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
/// <https://github.com/h3r2tic/tony-mc-mapface>
/// 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 [BezoldBrü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::<RenderDevice>()
.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<PipelineCache>,
mut pipelines: ResMut<SpecializedRenderPipelines<TonemappingPipeline>>,
upscaling_pipeline: Res<TonemappingPipeline>,
view_targets: Query<(Entity, &Tonemapping)>,
view_targets: Query<(Entity, Option<&Tonemapping>, Option<&DebandDither>), With<ViewTarget>>,
) {
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<Camera>)]
#[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<Image>,
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,
}
}

View file

@ -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<ExtractedView>>,
query: QueryState<
(
&'static ViewUniformOffset,
&'static ViewTarget,
&'static ViewTonemappingPipeline,
&'static Tonemapping,
),
With<ExtractedView>,
>,
cached_texture_bind_group: Mutex<Option<(TextureViewId, BindGroup)>>,
last_tonemapping: Mutex<Option<Tonemapping>>,
}
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::<PipelineCache>();
let tonemapping_pipeline = world.resource::<TonemappingPipeline>();
let gpu_images = world.get_resource::<RenderAssets<Image>>().unwrap();
let view_uniforms_resource = world.resource::<ViewUniforms>();
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::<TonemappingLuts>();
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(())

View file

@ -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<f32>;
var<uniform> view: View;
@group(0) @binding(1)
var hdr_texture: texture_2d<f32>;
@group(0) @binding(2)
var hdr_sampler: sampler;
@group(0) @binding(3)
var dt_lut_texture: texture_3d<f32>;
@group(0) @binding(4)
var dt_lut_sampler: sampler;
#import bevy_core_pipeline::tonemapping
@fragment
fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4<f32> {
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<f32>(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<f32>(2.2));
output_rgb = powsafe(output_rgb.rgb, 2.2);
#endif
return vec4<f32>(output_rgb, hdr_color.a);

View file

@ -1,5 +1,235 @@
#define_import_path bevy_core_pipeline::tonemapping
fn sample_current_lut(p: vec3<f32>) -> vec3<f32> {
// 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<f32>) -> vec3<f32> {
let m = mat3x3<f32>(
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<f32>) -> vec3<f32> {
let m = mat3x3<f32>(
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<f32>) -> vec3<f32> {
return vec3(tonemap_curve(v.r), tonemap_curve(v.g), tonemap_curve(v.b));
}
fn somewhat_boring_display_transform(col: vec3<f32>) -> vec3<f32> {
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<f32>(-13.0, 8.0);
const TONY_MC_MAPFACE_LUT_DIMS: f32 = 48.0;
fn tony_mc_mapface_lut_range_encode(x: vec3<f32>) -> vec3<f32> {
return x / (x + 1.0);
}
fn sample_tony_mc_mapface_lut(stimulus: vec3<f32>) -> vec3<f32> {
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<f32>) -> vec3<f32> {
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<f32>) -> vec3<f32> {
var color = color;
// sRGB => XYZ => D65_2_D60 => AP1 => RRT_SAT
let rgb_to_rrt = mat3x3<f32>(
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<f32>(
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<f32>, power: f32) -> vec3<f32> {
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<f32>, saturationAmount: f32) -> vec3<f32> {
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<f32>, minimum_ev: f32, maximum_ev: f32) -> vec3<f32> {
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<f32>, minimum_ev: f32, maximum_ev: f32) -> vec3<f32> {
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<f32>) -> vec3<f32> {
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<f32>, block_size: f32) -> vec3<f32> {
return sample_current_lut(Image * ((block_size - 1.0) / block_size) + 0.5 / block_size).rgb;
}
// -------------------------
// -------------------------
// -------------------------
fn sample_blender_filmic_lut(stimulus: vec3<f32>) -> vec3<f32> {
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<f32>) -> vec3<f32> {
@ -22,7 +252,7 @@ fn tonemapping_change_luminance(c_in: vec3<f32>, l_out: f32) -> vec3<f32> {
return c_in * (l_out / l_in);
}
fn reinhard_luminance(color: vec3<f32>) -> vec3<f32> {
fn tonemapping_reinhard_luminance(color: vec3<f32>) -> vec3<f32> {
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<f32>) -> vec3<f32> {
dither = fract(dither.rgb / vec3<f32>(103.0, 71.0, 97.0));
return (dither - 0.5) / 255.0;
}
fn tone_mapping(in: vec4<f32>) -> vec4<f32> {
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);
}

View file

@ -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"]

View file

@ -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<M: Material>(
&ExtractedView,
&VisibleEntities,
Option<&Tonemapping>,
Option<&DebandDither>,
Option<&EnvironmentMapLight>,
&mut RenderPhase<Opaque3d>,
&mut RenderPhase<AlphaMask3d>,
@ -379,6 +380,7 @@ pub fn queue_material_meshes<M: Material>(
view,
visible_entities,
tonemapping,
dither,
environment_map,
mut opaque_phase,
mut alpha_mask_phase,
@ -400,13 +402,26 @@ pub fn queue_material_meshes<M: Material>(
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;
}
}

View file

@ -1150,6 +1150,7 @@ pub fn prepare_lights(
view_projection: None,
projection: cube_face_projection,
hdr: false,
color_grading: Default::default(),
},
RenderPhase::<Shadow>::default(),
LightEntity::Point {
@ -1207,6 +1208,7 @@ pub fn prepare_lights(
projection: spot_projection,
view_projection: None,
hdr: false,
color_grading: Default::default(),
},
RenderPhase::<Shadow>::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::<Shadow>::default(),
LightEntity::Directional {

View file

@ -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<RenderAssets<Image>>,
mut fallback_images: FallbackImagesMsaa,
@ -926,6 +968,7 @@ pub fn queue_mesh_view_bind_groups(
fallback_cubemap: Res<FallbackImageCubemap>,
msaa: Res<Msaa>,
globals_buffer: Res<GlobalsBuffer>,
tonemapping_luts: Res<TonemappingLuts>,
) {
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),
});
}

View file

@ -53,14 +53,19 @@ var environment_map_specular: texture_cube<f32>;
@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<f32>;
@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<f32>;
#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<f32>;
#endif

View file

@ -107,11 +107,11 @@ fn fragment(in: FragmentInput) -> @location(0) vec4<f32> {
#endif
#ifdef DEBAND_DITHER
var output_rgb = output_color.rgb;
output_rgb = pow(output_rgb, vec3<f32>(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<f32>(2.2));
output_rgb = powsafe(output_rgb, 2.2);
output_color = vec4(output_rgb, output_color.a);
#endif
#ifdef PREMULTIPLY_ALPHA

View file

@ -267,17 +267,6 @@ fn pbr(
}
#endif // NORMAL_PREPASS
#ifdef TONEMAP_IN_SHADER
fn tone_mapping(in: vec4<f32>) -> vec4<f32> {
// tone_mapping
return vec4<f32>(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<f32>, pos: vec2<f32>) -> vec4<f32> {
return vec4<f32>(color.rgb + screen_space_dither(pos.xy), color.a);

View file

@ -10,6 +10,7 @@ keywords = ["bevy"]
[features]
png = ["image/png"]
exr = ["image/exr"]
hdr = ["image/hdr"]
tga = ["image/tga"]
jpeg = ["image/jpeg"]

View file

@ -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<Query<Entity, With<PrimaryWindow>>>,
) {
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(),
));

View file

@ -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"]
}
}

View file

@ -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,

View file

@ -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 {

View file

@ -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::<ImageTextureLoader>();
}
#[cfg(feature = "exr")]
{
app.init_asset_loader::<ExrTextureLoader>();
}
#[cfg(feature = "hdr")]
{
app.init_asset_loader::<HdrTextureLoader>();

View file

@ -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,
}),
};

View file

@ -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<f32>,
inverse_view_proj: mat4x4<f32>,
@ -10,4 +17,5 @@ struct View {
world_position: vec3<f32>,
// viewport(x_origin, y_origin, width, height)
viewport: vec4<f32>,
color_grading: ColorGrading,
};

View file

@ -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<f32>,
// '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<f32> {
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;
}

View file

@ -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<M: Material2d>(
&ExtractedView,
&VisibleEntities,
Option<&Tonemapping>,
Option<&DebandDither>,
&mut RenderPhase<Transparent2d>,
)>,
) where
@ -336,19 +340,32 @@ pub fn queue_material2d_meshes<M: Material2d>(
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::<DrawMaterial2d<M>>();
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;
}
}

View file

@ -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());

View file

@ -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<f32>,
@ -61,7 +65,11 @@ struct FragmentInput {
@fragment
fn fragment(in: FragmentInput) -> @location(0) vec4<f32> {
#ifdef VERTEX_COLORS
return in.color;
var color = in.color;
#ifdef TONEMAP_IN_SHADER
color = tone_mapping(color);
#endif
return color;
#else
return vec4<f32>(1.0, 0.0, 1.0, 1.0);
#endif

View file

@ -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<SpriteAssetEvents>,
) {
@ -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,

View file

@ -45,7 +45,7 @@ fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
#endif
#ifdef TONEMAP_IN_SHADER
color = vec4<f32>(reinhard_luminance(color.rgb), color.a);
color = tone_mapping(color);
#endif
return color;

View file

@ -277,6 +277,7 @@ pub fn extract_default_ui_camera_view<T: Component>(
physical_size.x,
physical_size.y,
),
color_grading: Default::default(),
})
.id();
commands.get_or_spawn(entity).insert((

697
examples/3d/tonemapping.rs Normal file
View file

@ -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::<ColorGradientMaterial>::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::<PerMethodSettings>()
.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<AssetServer>,
camera_transform: Res<CameraTransform>,
) {
// 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<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
mut images: ResMut<Assets<Image>>,
asset_server: Res<AssetServer>,
) {
// 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<Assets<Mesh>>,
mut materials: ResMut<Assets<ColorGradientMaterial>>,
camera_transform: Res<CameraTransform>,
) {
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<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
camera_transform: Res<CameraTransform>,
asset_server: Res<AssetServer>,
) {
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<StandardMaterial>, &Handle<Mesh>), With<HDRViewer>>,
text: Query<Entity, (With<Text>, With<SceneNumber>)>,
mut materials: ResMut<Assets<StandardMaterial>>,
mut meshes: ResMut<Assets<Mesh>>,
images: Res<Assets<Image>>,
mut drop_events: EventReader<FileDragAndDrop>,
mut drop_hovered: Local<bool>,
asset_server: Res<AssetServer>,
mut image_events: EventReader<AssetEvent<Image>>,
mut commands: Commands,
) {
let mut new_image: Option<Handle<Image>> = 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<Input<KeyCode>>,
mut query: Query<(&mut Visibility, &SceneNumber)>,
mut current_scene: ResMut<CurrentScene>,
) {
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<Input<KeyCode>>,
mut tonemapping: Query<&mut Tonemapping>,
mut color_grading: Query<&mut ColorGrading>,
per_method_settings: Res<PerMethodSettings>,
) {
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<Input<KeyCode>>,
time: Res<Time>,
mut per_method_settings: ResMut<PerMethodSettings>,
tonemapping: Query<&Tonemapping>,
current_scene: Res<CurrentScene>,
mut selected_parameter: ResMut<SelectedParameter>,
) {
let method = tonemapping.single();
let mut color_grading = per_method_settings.settings.get_mut(method).unwrap();
let mut dt = time.delta_seconds() * 0.25;
if keys.pressed(KeyCode::Left) {
dt = -dt;
}
if keys.just_pressed(KeyCode::Down) {
selected_parameter.next();
}
if keys.just_pressed(KeyCode::Up) {
selected_parameter.prev();
}
if keys.pressed(KeyCode::Left) || keys.pressed(KeyCode::Right) {
match selected_parameter.value {
0 => {
color_grading.exposure += dt;
}
1 => {
color_grading.gamma += dt;
}
2 => {
color_grading.pre_saturation += dt;
}
3 => {
color_grading.post_saturation += dt;
}
_ => {}
}
}
if keys.just_pressed(KeyCode::Space) {
for (_, grading) in per_method_settings.settings.iter_mut() {
*grading = ColorGrading::default();
}
}
if keys.just_pressed(KeyCode::Return) && current_scene.0 == 1 {
for (mapper, grading) in per_method_settings.settings.iter_mut() {
*grading = PerMethodSettings::basic_scene_recommendation(*mapper);
}
}
}
fn update_ui(
mut text: Query<&mut Text, Without<SceneNumber>>,
settings: Query<(&Tonemapping, &ColorGrading)>,
current_scene: Res<CurrentScene>,
selected_parameter: Res<SelectedParameter>,
mut hide_ui: Local<bool>,
keys: Res<Input<KeyCode>>,
) {
let (method, color_grading) = settings.single();
let method = *method;
let mut text = text.single_mut();
let text = &mut text.sections[0].value;
if keys.just_pressed(KeyCode::H) {
*hide_ui = !*hide_ui;
}
text.clear();
if *hide_ui {
return;
}
let scn = current_scene.0;
text.push_str("(H) Hide UI\n\n");
text.push_str("Test Scene: \n");
text.push_str(&format!(
"(Q) {} Basic Scene\n",
if scn == 1 { ">" } else { "" }
));
text.push_str(&format!(
"(W) {} Color Sweep\n",
if scn == 2 { ">" } else { "" }
));
text.push_str(&format!(
"(E) {} Image Viewer\n",
if scn == 3 { ">" } else { "" }
));
text.push_str("\n\nTonemapping Method:\n");
text.push_str(&format!(
"(1) {} Disabled\n",
if method == Tonemapping::None { ">" } else { "" }
));
text.push_str(&format!(
"(2) {} Reinhard\n",
if method == Tonemapping::Reinhard {
"> "
} else {
""
}
));
text.push_str(&format!(
"(3) {} Reinhard Luminance\n",
if method == Tonemapping::ReinhardLuminance {
">"
} else {
""
}
));
text.push_str(&format!(
"(4) {} ACES Fitted\n",
if method == Tonemapping::AcesFitted {
">"
} else {
""
}
));
text.push_str(&format!(
"(5) {} AgX\n",
if method == Tonemapping::AgX { ">" } else { "" }
));
text.push_str(&format!(
"(6) {} SomewhatBoringDisplayTransform\n",
if method == Tonemapping::SomewhatBoringDisplayTransform {
">"
} else {
""
}
));
text.push_str(&format!(
"(7) {} TonyMcMapface\n",
if method == Tonemapping::TonyMcMapface {
">"
} else {
""
}
));
text.push_str(&format!(
"(8) {} Blender Filmic\n",
if method == Tonemapping::BlenderFilmic {
">"
} else {
""
}
));
text.push_str("\n\nColor Grading:\n");
text.push_str("(arrow keys)\n");
if selected_parameter.value == 0 {
text.push_str("> ");
}
text.push_str(&format!("Exposure: {}\n", color_grading.exposure));
if selected_parameter.value == 1 {
text.push_str("> ");
}
text.push_str(&format!("Gamma: {}\n", color_grading.gamma));
if selected_parameter.value == 2 {
text.push_str("> ");
}
text.push_str(&format!(
"PreSaturation: {}\n",
color_grading.pre_saturation
));
if selected_parameter.value == 3 {
text.push_str("> ");
}
text.push_str(&format!(
"PostSaturation: {}\n",
color_grading.post_saturation
));
text.push_str("(Space) Reset all to default\n");
if current_scene.0 == 1 {
text.push_str("(Enter) Reset all to scene recommendation\n");
}
}
// ----------------------------------------------------------------------------
#[derive(Resource)]
struct PerMethodSettings {
settings: HashMap<Tonemapping, ColorGrading>,
}
impl PerMethodSettings {
fn basic_scene_recommendation(method: Tonemapping) -> ColorGrading {
match method {
Tonemapping::Reinhard | Tonemapping::ReinhardLuminance => ColorGrading {
exposure: 0.5,
..default()
},
Tonemapping::AcesFitted => ColorGrading {
exposure: 0.35,
..default()
},
Tonemapping::AgX => ColorGrading {
exposure: -0.2,
gamma: 1.0,
pre_saturation: 1.1,
post_saturation: 1.1,
},
_ => ColorGrading::default(),
}
}
}
impl Default for PerMethodSettings {
fn default() -> Self {
let mut settings = HashMap::new();
for method in [
Tonemapping::None,
Tonemapping::Reinhard,
Tonemapping::ReinhardLuminance,
Tonemapping::AcesFitted,
Tonemapping::AgX,
Tonemapping::SomewhatBoringDisplayTransform,
Tonemapping::TonyMcMapface,
Tonemapping::BlenderFilmic,
] {
settings.insert(
method,
PerMethodSettings::basic_scene_recommendation(method),
);
}
Self { settings }
}
}
/// Creates a colorful test pattern
fn uv_debug_texture() -> Image {
const TEXTURE_SIZE: usize = 8;
let mut palette: [u8; 32] = [
255, 102, 159, 255, 255, 159, 102, 255, 236, 255, 102, 255, 121, 255, 102, 255, 102, 255,
198, 255, 102, 198, 255, 255, 121, 102, 255, 255, 236, 102, 255, 255,
];
let mut texture_data = [0; TEXTURE_SIZE * TEXTURE_SIZE * 4];
for y in 0..TEXTURE_SIZE {
let offset = TEXTURE_SIZE * y * 4;
texture_data[offset..(offset + TEXTURE_SIZE * 4)].copy_from_slice(&palette);
palette.rotate_right(4);
}
let mut img = Image::new_fill(
Extent3d {
width: TEXTURE_SIZE as u32,
height: TEXTURE_SIZE as u32,
depth_or_array_layers: 1,
},
TextureDimension::D2,
&texture_data,
TextureFormat::Rgba8UnormSrgb,
);
img.sampler_descriptor = ImageSampler::Descriptor(SamplerDescriptor::default());
img
}
impl Material for ColorGradientMaterial {
fn fragment_shader() -> ShaderRef {
"shaders/tonemapping_test_patterns.wgsl".into()
}
}
#[derive(AsBindGroup, Debug, Clone, TypeUuid)]
#[uuid = "117f64fe-6844-1822-8926-e3ed372291c8"]
pub struct ColorGradientMaterial {}
#[derive(Resource)]
struct CameraTransform(Transform);
#[derive(Resource)]
struct CurrentScene(u32);
#[derive(Component)]
struct SceneNumber(u32);
#[derive(Component)]
struct HDRViewer;

View file

@ -127,6 +127,7 @@ Example | Description
[Split Screen](../examples/3d/split_screen.rs) | Demonstrates how to render two cameras to the same window to accomplish "split screen"
[Spotlight](../examples/3d/spotlight.rs) | Illustrates spot lights
[Texture](../examples/3d/texture.rs) | Shows configuration of texture materials
[Tonemapping](../examples/3d/tonemapping.rs) | Compares tonemapping options
[Transparency in 3D](../examples/3d/transparency_3d.rs) | Demonstrates transparency in 3d
[Two Passes](../examples/3d/two_passes.rs) | Renders two 3d passes to the same window from different perspectives
[Update glTF Scene](../examples/3d/update_gltf_scene.rs) | Update a scene from a glTF file, either by spawning the scene as a child of another entity, or by accessing the entities of the scene