mirror of
https://github.com/bevyengine/bevy
synced 2024-11-10 07:04:33 +00:00
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:
parent
d46427b4e4
commit
912fb58869
37 changed files with 1849 additions and 154 deletions
16
Cargo.toml
16
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"
|
||||
|
|
64
assets/shaders/tonemapping_test_patterns.wgsl
Normal file
64
assets/shaders/tonemapping_test_patterns.wgsl
Normal 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;
|
||||
}
|
|
@ -15,6 +15,7 @@ keywords = ["bevy"]
|
|||
[features]
|
||||
trace = []
|
||||
webgl = []
|
||||
tonemapping_luts = []
|
||||
|
||||
[dependencies]
|
||||
# bevy
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Binary file not shown.
Binary file not shown.
22
crates/bevy_core_pipeline/src/tonemapping/luts/info.txt
Normal file
22
crates/bevy_core_pipeline/src/tonemapping/luts/info.txt
Normal 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
|
Binary file not shown.
|
@ -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 [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::<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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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),
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -10,6 +10,7 @@ keywords = ["bevy"]
|
|||
|
||||
[features]
|
||||
png = ["image/png"]
|
||||
exr = ["image/exr"]
|
||||
hdr = ["image/hdr"]
|
||||
tga = ["image/tga"]
|
||||
jpeg = ["image/jpeg"]
|
||||
|
|
|
@ -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(),
|
||||
));
|
||||
|
|
56
crates/bevy_render/src/texture/exr_texture_loader.rs
Normal file
56
crates/bevy_render/src/texture/exr_texture_loader.rs
Normal 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"]
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>();
|
||||
|
|
|
@ -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,
|
||||
}),
|
||||
};
|
||||
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
697
examples/3d/tonemapping.rs
Normal 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;
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue