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
16
Cargo.toml
16
Cargo.toml
@ -50,6 +50,7 @@ default = [
|
|||||||
"x11",
|
"x11",
|
||||||
"filesystem_watcher",
|
"filesystem_watcher",
|
||||||
"android_shared_stdcxx",
|
"android_shared_stdcxx",
|
||||||
|
"tonemapping_luts"
|
||||||
]
|
]
|
||||||
|
|
||||||
# Force dynamic linking, which improves iterative compile times
|
# Force dynamic linking, which improves iterative compile times
|
||||||
@ -78,6 +79,7 @@ trace = ["bevy_internal/trace"]
|
|||||||
wgpu_trace = ["bevy_internal/wgpu_trace"]
|
wgpu_trace = ["bevy_internal/wgpu_trace"]
|
||||||
|
|
||||||
# Image format support for texture loading (PNG and HDR are enabled by default)
|
# Image format support for texture loading (PNG and HDR are enabled by default)
|
||||||
|
exr = ["bevy_internal/exr"]
|
||||||
hdr = ["bevy_internal/hdr"]
|
hdr = ["bevy_internal/hdr"]
|
||||||
png = ["bevy_internal/png"]
|
png = ["bevy_internal/png"]
|
||||||
tga = ["bevy_internal/tga"]
|
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.
|
# These trace events are expensive even when off, thus they require compile time opt-in.
|
||||||
detailed_trace = ["bevy_internal/detailed_trace"]
|
detailed_trace = ["bevy_internal/detailed_trace"]
|
||||||
|
|
||||||
|
# Include tonemapping LUT KTX2 files.
|
||||||
|
tonemapping_luts = ["bevy_internal/tonemapping_luts"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bevy_dylib = { path = "crates/bevy_dylib", version = "0.9.0", default-features = false, optional = true }
|
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 }
|
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"
|
category = "3D Rendering"
|
||||||
wasm = true
|
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]]
|
[[example]]
|
||||||
name = "fxaa"
|
name = "fxaa"
|
||||||
path = "examples/3d/fxaa.rs"
|
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]
|
[features]
|
||||||
trace = []
|
trace = []
|
||||||
webgl = []
|
webgl = []
|
||||||
|
tonemapping_luts = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# bevy
|
# 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_ecs::prelude::*;
|
||||||
use bevy_reflect::Reflect;
|
use bevy_reflect::Reflect;
|
||||||
use bevy_render::{
|
use bevy_render::{
|
||||||
@ -27,6 +30,7 @@ pub struct Camera2dBundle {
|
|||||||
pub global_transform: GlobalTransform,
|
pub global_transform: GlobalTransform,
|
||||||
pub camera_2d: Camera2d,
|
pub camera_2d: Camera2d,
|
||||||
pub tonemapping: Tonemapping,
|
pub tonemapping: Tonemapping,
|
||||||
|
pub deband_dither: DebandDither,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Camera2dBundle {
|
impl Default for Camera2dBundle {
|
||||||
@ -67,7 +71,8 @@ impl Camera2dBundle {
|
|||||||
global_transform: Default::default(),
|
global_transform: Default::default(),
|
||||||
camera: Camera::default(),
|
camera: Camera::default(),
|
||||||
camera_2d: Camera2d::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_ecs::prelude::*;
|
||||||
use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize};
|
use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize};
|
||||||
use bevy_render::{
|
use bevy_render::{
|
||||||
@ -6,7 +9,7 @@ use bevy_render::{
|
|||||||
extract_component::ExtractComponent,
|
extract_component::ExtractComponent,
|
||||||
primitives::Frustum,
|
primitives::Frustum,
|
||||||
render_resource::LoadOp,
|
render_resource::LoadOp,
|
||||||
view::VisibleEntities,
|
view::{ColorGrading, VisibleEntities},
|
||||||
};
|
};
|
||||||
use bevy_transform::prelude::{GlobalTransform, Transform};
|
use bevy_transform::prelude::{GlobalTransform, Transform};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@ -59,6 +62,8 @@ pub struct Camera3dBundle {
|
|||||||
pub global_transform: GlobalTransform,
|
pub global_transform: GlobalTransform,
|
||||||
pub camera_3d: Camera3d,
|
pub camera_3d: Camera3d,
|
||||||
pub tonemapping: Tonemapping,
|
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
|
// 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 {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
camera_render_graph: CameraRenderGraph::new(crate::core_3d::graph::NAME),
|
camera_render_graph: CameraRenderGraph::new(crate::core_3d::graph::NAME),
|
||||||
tonemapping: Tonemapping::Enabled {
|
|
||||||
deband_dither: true,
|
|
||||||
},
|
|
||||||
camera: Default::default(),
|
camera: Default::default(),
|
||||||
projection: Default::default(),
|
projection: Default::default(),
|
||||||
visible_entities: Default::default(),
|
visible_entities: Default::default(),
|
||||||
@ -76,6 +78,9 @@ impl Default for Camera3dBundle {
|
|||||||
transform: Default::default(),
|
transform: Default::default(),
|
||||||
global_transform: Default::default(),
|
global_transform: Default::default(),
|
||||||
camera_3d: 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 crate::fullscreen_vertex_shader::fullscreen_shader_vertex_state;
|
||||||
use bevy_app::prelude::*;
|
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_ecs::prelude::*;
|
||||||
use bevy_reflect::{Reflect, TypeUuid};
|
use bevy_reflect::{FromReflect, Reflect, TypeUuid};
|
||||||
use bevy_render::camera::Camera;
|
use bevy_render::camera::Camera;
|
||||||
use bevy_render::extract_component::{ExtractComponent, ExtractComponentPlugin};
|
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::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};
|
use bevy_render::{render_resource::*, RenderApp, RenderSet};
|
||||||
|
|
||||||
mod node;
|
mod node;
|
||||||
|
|
||||||
|
use bevy_utils::default;
|
||||||
pub use node::TonemappingNode;
|
pub use node::TonemappingNode;
|
||||||
|
|
||||||
const TONEMAPPING_SHADER_HANDLE: HandleUntyped =
|
const TONEMAPPING_SHADER_HANDLE: HandleUntyped =
|
||||||
@ -19,6 +23,14 @@ const TONEMAPPING_SHADER_HANDLE: HandleUntyped =
|
|||||||
const TONEMAPPING_SHARED_SHADER_HANDLE: HandleUntyped =
|
const TONEMAPPING_SHARED_SHADER_HANDLE: HandleUntyped =
|
||||||
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 2499430578245347910);
|
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;
|
pub struct TonemappingPlugin;
|
||||||
|
|
||||||
impl Plugin for TonemappingPlugin {
|
impl Plugin for TonemappingPlugin {
|
||||||
@ -36,9 +48,47 @@ impl Plugin for TonemappingPlugin {
|
|||||||
Shader::from_wgsl
|
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::<Tonemapping>();
|
||||||
|
app.register_type::<DebandDither>();
|
||||||
|
|
||||||
app.add_plugin(ExtractComponentPlugin::<Tonemapping>::default());
|
app.add_plugin(ExtractComponentPlugin::<Tonemapping>::default());
|
||||||
|
app.add_plugin(ExtractComponentPlugin::<DebandDither>::default());
|
||||||
|
|
||||||
if let Ok(render_app) = app.get_sub_app_mut(RenderApp) {
|
if let Ok(render_app) = app.get_sub_app_mut(RenderApp) {
|
||||||
render_app
|
render_app
|
||||||
@ -54,9 +104,77 @@ pub struct TonemappingPipeline {
|
|||||||
texture_bind_group: BindGroupLayout,
|
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 {
|
pub struct TonemappingPipelineKey {
|
||||||
deband_dither: bool,
|
deband_dither: DebandDither,
|
||||||
|
tonemapping: Tonemapping,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SpecializedRenderPipeline for TonemappingPipeline {
|
impl SpecializedRenderPipeline for TonemappingPipeline {
|
||||||
@ -64,9 +182,25 @@ impl SpecializedRenderPipeline for TonemappingPipeline {
|
|||||||
|
|
||||||
fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
|
fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
|
||||||
let mut shader_defs = Vec::new();
|
let mut shader_defs = Vec::new();
|
||||||
if key.deband_dither {
|
if let DebandDither::Enabled = key.deband_dither {
|
||||||
shader_defs.push("DEBAND_DITHER".into());
|
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 {
|
RenderPipelineDescriptor {
|
||||||
label: Some("tonemapping pipeline".into()),
|
label: Some("tonemapping pipeline".into()),
|
||||||
layout: vec![self.texture_bind_group.clone()],
|
layout: vec![self.texture_bind_group.clone()],
|
||||||
@ -91,14 +225,20 @@ impl SpecializedRenderPipeline for TonemappingPipeline {
|
|||||||
|
|
||||||
impl FromWorld for TonemappingPipeline {
|
impl FromWorld for TonemappingPipeline {
|
||||||
fn from_world(render_world: &mut World) -> Self {
|
fn from_world(render_world: &mut World) -> Self {
|
||||||
let tonemap_texture_bind_group = render_world
|
let mut entries = vec![
|
||||||
.resource::<RenderDevice>()
|
|
||||||
.create_bind_group_layout(&BindGroupLayoutDescriptor {
|
|
||||||
label: Some("tonemapping_hdr_texture_bind_group_layout"),
|
|
||||||
entries: &[
|
|
||||||
BindGroupLayoutEntry {
|
BindGroupLayoutEntry {
|
||||||
binding: 0,
|
binding: 0,
|
||||||
visibility: ShaderStages::FRAGMENT,
|
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 {
|
ty: BindingType::Texture {
|
||||||
sample_type: TextureSampleType::Float { filterable: false },
|
sample_type: TextureSampleType::Float { filterable: false },
|
||||||
view_dimension: TextureViewDimension::D2,
|
view_dimension: TextureViewDimension::D2,
|
||||||
@ -107,12 +247,19 @@ impl FromWorld for TonemappingPipeline {
|
|||||||
count: None,
|
count: None,
|
||||||
},
|
},
|
||||||
BindGroupLayoutEntry {
|
BindGroupLayoutEntry {
|
||||||
binding: 1,
|
binding: 2,
|
||||||
visibility: ShaderStages::FRAGMENT,
|
visibility: ShaderStages::FRAGMENT,
|
||||||
ty: BindingType::Sampler(SamplerBindingType::NonFiltering),
|
ty: BindingType::Sampler(SamplerBindingType::NonFiltering),
|
||||||
count: None,
|
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: &entries,
|
||||||
});
|
});
|
||||||
|
|
||||||
TonemappingPipeline {
|
TonemappingPipeline {
|
||||||
@ -129,12 +276,12 @@ pub fn queue_view_tonemapping_pipelines(
|
|||||||
pipeline_cache: Res<PipelineCache>,
|
pipeline_cache: Res<PipelineCache>,
|
||||||
mut pipelines: ResMut<SpecializedRenderPipelines<TonemappingPipeline>>,
|
mut pipelines: ResMut<SpecializedRenderPipelines<TonemappingPipeline>>,
|
||||||
upscaling_pipeline: Res<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() {
|
for (entity, tonemapping, dither) in view_targets.iter() {
|
||||||
if let Tonemapping::Enabled { deband_dither } = tonemapping {
|
|
||||||
let key = TonemappingPipelineKey {
|
let key = TonemappingPipelineKey {
|
||||||
deband_dither: *deband_dither,
|
deband_dither: *dither.unwrap_or(&DebandDither::Disabled),
|
||||||
|
tonemapping: *tonemapping.unwrap_or(&Tonemapping::None),
|
||||||
};
|
};
|
||||||
let pipeline = pipelines.specialize(&pipeline_cache, &upscaling_pipeline, key);
|
let pipeline = pipelines.specialize(&pipeline_cache, &upscaling_pipeline, key);
|
||||||
|
|
||||||
@ -142,22 +289,120 @@ pub fn queue_view_tonemapping_pipelines(
|
|||||||
.entity(entity)
|
.entity(entity)
|
||||||
.insert(ViewTonemappingPipeline(pipeline));
|
.insert(ViewTonemappingPipeline(pipeline));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
/// Enables a debanding shader that applies dithering to mitigate color banding in the final image for a given [`Camera`] entity.
|
||||||
#[derive(Component, Clone, Reflect, Default, ExtractComponent)]
|
#[derive(
|
||||||
|
Component,
|
||||||
|
Debug,
|
||||||
|
Hash,
|
||||||
|
Clone,
|
||||||
|
Copy,
|
||||||
|
Reflect,
|
||||||
|
Default,
|
||||||
|
ExtractComponent,
|
||||||
|
PartialEq,
|
||||||
|
Eq,
|
||||||
|
FromReflect,
|
||||||
|
)]
|
||||||
#[extract_component_filter(With<Camera>)]
|
#[extract_component_filter(With<Camera>)]
|
||||||
#[reflect(Component)]
|
#[reflect(Component)]
|
||||||
pub enum Tonemapping {
|
pub enum DebandDither {
|
||||||
#[default]
|
#[default]
|
||||||
Disabled,
|
Disabled,
|
||||||
Enabled {
|
Enabled,
|
||||||
deband_dither: bool,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Tonemapping {
|
pub fn get_lut_bindings<'a>(
|
||||||
pub fn is_enabled(&self) -> bool {
|
images: &'a RenderAssets<Image>,
|
||||||
matches!(self, Tonemapping::Enabled { .. })
|
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 std::sync::Mutex;
|
||||||
|
|
||||||
use crate::tonemapping::{TonemappingPipeline, ViewTonemappingPipeline};
|
use crate::tonemapping::{TonemappingLuts, TonemappingPipeline, ViewTonemappingPipeline};
|
||||||
|
|
||||||
use bevy_ecs::prelude::*;
|
use bevy_ecs::prelude::*;
|
||||||
use bevy_ecs::query::QueryState;
|
use bevy_ecs::query::QueryState;
|
||||||
use bevy_render::{
|
use bevy_render::{
|
||||||
|
render_asset::RenderAssets,
|
||||||
render_graph::{Node, NodeRunError, RenderGraphContext, SlotInfo, SlotType},
|
render_graph::{Node, NodeRunError, RenderGraphContext, SlotInfo, SlotType},
|
||||||
render_resource::{
|
render_resource::{
|
||||||
BindGroup, BindGroupDescriptor, BindGroupEntry, BindingResource, LoadOp, Operations,
|
BindGroup, BindGroupDescriptor, BindGroupEntry, BindingResource, LoadOp, Operations,
|
||||||
@ -11,12 +13,24 @@ use bevy_render::{
|
|||||||
TextureViewId,
|
TextureViewId,
|
||||||
},
|
},
|
||||||
renderer::RenderContext,
|
renderer::RenderContext,
|
||||||
view::{ExtractedView, ViewTarget},
|
texture::Image,
|
||||||
|
view::{ExtractedView, ViewTarget, ViewUniformOffset, ViewUniforms},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use super::{get_lut_bindings, Tonemapping};
|
||||||
|
|
||||||
pub struct TonemappingNode {
|
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)>>,
|
cached_texture_bind_group: Mutex<Option<(TextureViewId, BindGroup)>>,
|
||||||
|
last_tonemapping: Mutex<Option<Tonemapping>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TonemappingNode {
|
impl TonemappingNode {
|
||||||
@ -26,6 +40,7 @@ impl TonemappingNode {
|
|||||||
Self {
|
Self {
|
||||||
query: QueryState::new(world),
|
query: QueryState::new(world),
|
||||||
cached_texture_bind_group: Mutex::new(None),
|
cached_texture_bind_group: Mutex::new(None),
|
||||||
|
last_tonemapping: Mutex::new(None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -48,8 +63,12 @@ impl Node for TonemappingNode {
|
|||||||
let view_entity = graph.get_input_entity(Self::IN_VIEW)?;
|
let view_entity = graph.get_input_entity(Self::IN_VIEW)?;
|
||||||
let pipeline_cache = world.resource::<PipelineCache>();
|
let pipeline_cache = world.resource::<PipelineCache>();
|
||||||
let tonemapping_pipeline = world.resource::<TonemappingPipeline>();
|
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) {
|
let (view_uniform_offset, target, view_tonemapping_pipeline, tonemapping) =
|
||||||
|
match self.query.get_manual(world, view_entity) {
|
||||||
Ok(result) => result,
|
Ok(result) => result,
|
||||||
Err(_) => return Ok(()),
|
Err(_) => return Ok(()),
|
||||||
};
|
};
|
||||||
@ -58,7 +77,7 @@ impl Node for TonemappingNode {
|
|||||||
return Ok(());
|
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,
|
Some(pipeline) => pipeline,
|
||||||
None => return Ok(()),
|
None => return Ok(()),
|
||||||
};
|
};
|
||||||
@ -67,30 +86,56 @@ impl Node for TonemappingNode {
|
|||||||
let source = post_process.source;
|
let source = post_process.source;
|
||||||
let destination = post_process.destination;
|
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 mut cached_bind_group = self.cached_texture_bind_group.lock().unwrap();
|
||||||
let bind_group = match &mut *cached_bind_group {
|
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 => {
|
cached_bind_group => {
|
||||||
let sampler = render_context
|
let sampler = render_context
|
||||||
.render_device()
|
.render_device()
|
||||||
.create_sampler(&SamplerDescriptor::default());
|
.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 =
|
let bind_group =
|
||||||
render_context
|
render_context
|
||||||
.render_device()
|
.render_device()
|
||||||
.create_bind_group(&BindGroupDescriptor {
|
.create_bind_group(&BindGroupDescriptor {
|
||||||
label: None,
|
label: None,
|
||||||
layout: &tonemapping_pipeline.texture_bind_group,
|
layout: &tonemapping_pipeline.texture_bind_group,
|
||||||
entries: &[
|
entries: &entries,
|
||||||
BindGroupEntry {
|
|
||||||
binding: 0,
|
|
||||||
resource: BindingResource::TextureView(source),
|
|
||||||
},
|
|
||||||
BindGroupEntry {
|
|
||||||
binding: 1,
|
|
||||||
resource: BindingResource::Sampler(&sampler),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let (_, bind_group) = cached_bind_group.insert((source.id(), bind_group));
|
let (_, bind_group) = cached_bind_group.insert((source.id(), bind_group));
|
||||||
@ -116,7 +161,7 @@ impl Node for TonemappingNode {
|
|||||||
.begin_render_pass(&pass_descriptor);
|
.begin_render_pass(&pass_descriptor);
|
||||||
|
|
||||||
render_pass.set_pipeline(pipeline);
|
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);
|
render_pass.draw(0..3, 0..1);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -1,23 +1,33 @@
|
|||||||
#import bevy_core_pipeline::fullscreen_vertex_shader
|
#import bevy_core_pipeline::fullscreen_vertex_shader
|
||||||
#import bevy_core_pipeline::tonemapping
|
|
||||||
|
#import bevy_render::view
|
||||||
|
|
||||||
@group(0) @binding(0)
|
@group(0) @binding(0)
|
||||||
var hdr_texture: texture_2d<f32>;
|
var<uniform> view: View;
|
||||||
|
|
||||||
@group(0) @binding(1)
|
@group(0) @binding(1)
|
||||||
|
var hdr_texture: texture_2d<f32>;
|
||||||
|
@group(0) @binding(2)
|
||||||
var hdr_sampler: sampler;
|
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
|
@fragment
|
||||||
fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4<f32> {
|
fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4<f32> {
|
||||||
let hdr_color = textureSample(hdr_texture, hdr_sampler, in.uv);
|
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
|
#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);
|
output_rgb = output_rgb + screen_space_dither(in.position.xy);
|
||||||
// This conversion back to linear space is required because our output texture format is
|
// 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.
|
// 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
|
#endif
|
||||||
|
|
||||||
return vec4<f32>(output_rgb, hdr_color.a);
|
return vec4<f32>(output_rgb, hdr_color.a);
|
||||||
|
@ -1,5 +1,235 @@
|
|||||||
#define_import_path bevy_core_pipeline::tonemapping
|
#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/
|
// from https://64.github.io/tonemapping/
|
||||||
// reinhard on RGB oversaturates colors
|
// reinhard on RGB oversaturates colors
|
||||||
fn tonemapping_reinhard(color: vec3<f32>) -> vec3<f32> {
|
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);
|
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_old = tonemapping_luminance(color);
|
||||||
let l_new = l_old / (1.0 + l_old);
|
let l_new = l_old / (1.0 + l_old);
|
||||||
return tonemapping_change_luminance(color, l_new);
|
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));
|
dither = fract(dither.rgb / vec3<f32>(103.0, 71.0, 97.0));
|
||||||
return (dither - 0.5) / 255.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"]
|
detailed_trace = ["bevy_utils/detailed_trace"]
|
||||||
|
|
||||||
# Image format support for texture loading (PNG and HDR are enabled by default)
|
# Image format support for texture loading (PNG and HDR are enabled by default)
|
||||||
|
exr = ["bevy_render/exr"]
|
||||||
hdr = ["bevy_render/hdr"]
|
hdr = ["bevy_render/hdr"]
|
||||||
png = ["bevy_render/png"]
|
png = ["bevy_render/png"]
|
||||||
tga = ["bevy_render/tga"]
|
tga = ["bevy_render/tga"]
|
||||||
@ -38,6 +39,9 @@ ktx2 = ["bevy_render/ktx2"]
|
|||||||
zlib = ["bevy_render/zlib"]
|
zlib = ["bevy_render/zlib"]
|
||||||
zstd = ["bevy_render/zstd"]
|
zstd = ["bevy_render/zstd"]
|
||||||
|
|
||||||
|
# Include tonemapping LUT KTX2 files.
|
||||||
|
tonemapping_luts = ["bevy_core_pipeline/tonemapping_luts"]
|
||||||
|
|
||||||
# Audio format support (vorbis is enabled by default)
|
# Audio format support (vorbis is enabled by default)
|
||||||
flac = ["bevy_audio/flac"]
|
flac = ["bevy_audio/flac"]
|
||||||
mp3 = ["bevy_audio/mp3"]
|
mp3 = ["bevy_audio/mp3"]
|
||||||
|
@ -6,7 +6,7 @@ use bevy_app::{App, Plugin};
|
|||||||
use bevy_asset::{AddAsset, AssetEvent, AssetServer, Assets, Handle};
|
use bevy_asset::{AddAsset, AssetEvent, AssetServer, Assets, Handle};
|
||||||
use bevy_core_pipeline::{
|
use bevy_core_pipeline::{
|
||||||
core_3d::{AlphaMask3d, Opaque3d, Transparent3d},
|
core_3d::{AlphaMask3d, Opaque3d, Transparent3d},
|
||||||
tonemapping::Tonemapping,
|
tonemapping::{DebandDither, Tonemapping},
|
||||||
};
|
};
|
||||||
use bevy_derive::{Deref, DerefMut};
|
use bevy_derive::{Deref, DerefMut};
|
||||||
use bevy_ecs::{
|
use bevy_ecs::{
|
||||||
@ -367,6 +367,7 @@ pub fn queue_material_meshes<M: Material>(
|
|||||||
&ExtractedView,
|
&ExtractedView,
|
||||||
&VisibleEntities,
|
&VisibleEntities,
|
||||||
Option<&Tonemapping>,
|
Option<&Tonemapping>,
|
||||||
|
Option<&DebandDither>,
|
||||||
Option<&EnvironmentMapLight>,
|
Option<&EnvironmentMapLight>,
|
||||||
&mut RenderPhase<Opaque3d>,
|
&mut RenderPhase<Opaque3d>,
|
||||||
&mut RenderPhase<AlphaMask3d>,
|
&mut RenderPhase<AlphaMask3d>,
|
||||||
@ -379,6 +380,7 @@ pub fn queue_material_meshes<M: Material>(
|
|||||||
view,
|
view,
|
||||||
visible_entities,
|
visible_entities,
|
||||||
tonemapping,
|
tonemapping,
|
||||||
|
dither,
|
||||||
environment_map,
|
environment_map,
|
||||||
mut opaque_phase,
|
mut opaque_phase,
|
||||||
mut alpha_mask_phase,
|
mut alpha_mask_phase,
|
||||||
@ -400,13 +402,26 @@ pub fn queue_material_meshes<M: Material>(
|
|||||||
view_key |= MeshPipelineKey::ENVIRONMENT_MAP;
|
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;
|
view_key |= MeshPipelineKey::TONEMAP_IN_SHADER;
|
||||||
|
view_key |= match tonemapping {
|
||||||
if *deband_dither {
|
Tonemapping::None => MeshPipelineKey::TONEMAP_METHOD_NONE,
|
||||||
view_key |= MeshPipelineKey::DEBAND_DITHER;
|
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,
|
view_projection: None,
|
||||||
projection: cube_face_projection,
|
projection: cube_face_projection,
|
||||||
hdr: false,
|
hdr: false,
|
||||||
|
color_grading: Default::default(),
|
||||||
},
|
},
|
||||||
RenderPhase::<Shadow>::default(),
|
RenderPhase::<Shadow>::default(),
|
||||||
LightEntity::Point {
|
LightEntity::Point {
|
||||||
@ -1207,6 +1208,7 @@ pub fn prepare_lights(
|
|||||||
projection: spot_projection,
|
projection: spot_projection,
|
||||||
view_projection: None,
|
view_projection: None,
|
||||||
hdr: false,
|
hdr: false,
|
||||||
|
color_grading: Default::default(),
|
||||||
},
|
},
|
||||||
RenderPhase::<Shadow>::default(),
|
RenderPhase::<Shadow>::default(),
|
||||||
LightEntity::Spot { light_entity },
|
LightEntity::Spot { light_entity },
|
||||||
@ -1272,6 +1274,7 @@ pub fn prepare_lights(
|
|||||||
projection: cascade.projection,
|
projection: cascade.projection,
|
||||||
view_projection: Some(cascade.view_projection),
|
view_projection: Some(cascade.view_projection),
|
||||||
hdr: false,
|
hdr: false,
|
||||||
|
color_grading: Default::default(),
|
||||||
},
|
},
|
||||||
RenderPhase::<Shadow>::default(),
|
RenderPhase::<Shadow>::default(),
|
||||||
LightEntity::Directional {
|
LightEntity::Directional {
|
||||||
|
@ -6,7 +6,12 @@ use crate::{
|
|||||||
};
|
};
|
||||||
use bevy_app::Plugin;
|
use bevy_app::Plugin;
|
||||||
use bevy_asset::{load_internal_asset, Assets, Handle, HandleUntyped};
|
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::{
|
use bevy_ecs::{
|
||||||
prelude::*,
|
prelude::*,
|
||||||
query::ROQueryItem,
|
query::ROQueryItem,
|
||||||
@ -418,10 +423,14 @@ impl FromWorld for MeshPipeline {
|
|||||||
environment_map::get_bind_group_layout_entries([11, 12, 13]);
|
environment_map::get_bind_group_layout_entries([11, 12, 13]);
|
||||||
entries.extend_from_slice(&environment_map_entries);
|
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")) {
|
if cfg!(not(feature = "webgl")) {
|
||||||
// Depth texture
|
// Depth texture
|
||||||
entries.push(BindGroupLayoutEntry {
|
entries.push(BindGroupLayoutEntry {
|
||||||
binding: 14,
|
binding: 16,
|
||||||
visibility: ShaderStages::FRAGMENT,
|
visibility: ShaderStages::FRAGMENT,
|
||||||
ty: BindingType::Texture {
|
ty: BindingType::Texture {
|
||||||
multisampled,
|
multisampled,
|
||||||
@ -432,7 +441,7 @@ impl FromWorld for MeshPipeline {
|
|||||||
});
|
});
|
||||||
// Normal texture
|
// Normal texture
|
||||||
entries.push(BindGroupLayoutEntry {
|
entries.push(BindGroupLayoutEntry {
|
||||||
binding: 15,
|
binding: 17,
|
||||||
visibility: ShaderStages::FRAGMENT,
|
visibility: ShaderStages::FRAGMENT,
|
||||||
ty: BindingType::Texture {
|
ty: BindingType::Texture {
|
||||||
multisampled,
|
multisampled,
|
||||||
@ -588,6 +597,15 @@ bitflags::bitflags! {
|
|||||||
const BLEND_MULTIPLY = (2 << Self::BLEND_SHIFT_BITS); // ← We still have room for one more value without adding more 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 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 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_MASK_BITS: u32 = 0b11;
|
||||||
const BLEND_SHIFT_BITS: u32 =
|
const BLEND_SHIFT_BITS: u32 =
|
||||||
Self::PRIMITIVE_TOPOLOGY_SHIFT_BITS - Self::BLEND_MASK_BITS.count_ones();
|
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 {
|
pub fn from_msaa_samples(msaa_samples: u32) -> Self {
|
||||||
let msaa_bits =
|
let msaa_bits =
|
||||||
@ -743,6 +764,26 @@ impl SpecializedMeshPipeline for MeshPipeline {
|
|||||||
if key.contains(MeshPipelineKey::TONEMAP_IN_SHADER) {
|
if key.contains(MeshPipelineKey::TONEMAP_IN_SHADER) {
|
||||||
shader_defs.push("TONEMAP_IN_SHADER".into());
|
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.
|
// Debanding is tied to tonemapping in the shader, cannot run without it.
|
||||||
if key.contains(MeshPipelineKey::DEBAND_DITHER) {
|
if key.contains(MeshPipelineKey::DEBAND_DITHER) {
|
||||||
shader_defs.push("DEBAND_DITHER".into());
|
shader_defs.push("DEBAND_DITHER".into());
|
||||||
@ -919,6 +960,7 @@ pub fn queue_mesh_view_bind_groups(
|
|||||||
&ViewClusterBindings,
|
&ViewClusterBindings,
|
||||||
Option<&ViewPrepassTextures>,
|
Option<&ViewPrepassTextures>,
|
||||||
Option<&EnvironmentMapLight>,
|
Option<&EnvironmentMapLight>,
|
||||||
|
&Tonemapping,
|
||||||
)>,
|
)>,
|
||||||
images: Res<RenderAssets<Image>>,
|
images: Res<RenderAssets<Image>>,
|
||||||
mut fallback_images: FallbackImagesMsaa,
|
mut fallback_images: FallbackImagesMsaa,
|
||||||
@ -926,6 +968,7 @@ pub fn queue_mesh_view_bind_groups(
|
|||||||
fallback_cubemap: Res<FallbackImageCubemap>,
|
fallback_cubemap: Res<FallbackImageCubemap>,
|
||||||
msaa: Res<Msaa>,
|
msaa: Res<Msaa>,
|
||||||
globals_buffer: Res<GlobalsBuffer>,
|
globals_buffer: Res<GlobalsBuffer>,
|
||||||
|
tonemapping_luts: Res<TonemappingLuts>,
|
||||||
) {
|
) {
|
||||||
if let (
|
if let (
|
||||||
Some(view_binding),
|
Some(view_binding),
|
||||||
@ -946,6 +989,7 @@ pub fn queue_mesh_view_bind_groups(
|
|||||||
view_cluster_bindings,
|
view_cluster_bindings,
|
||||||
prepass_textures,
|
prepass_textures,
|
||||||
environment_map,
|
environment_map,
|
||||||
|
tonemapping,
|
||||||
) in &views
|
) in &views
|
||||||
{
|
{
|
||||||
let layout = if msaa.samples() > 1 {
|
let layout = if msaa.samples() > 1 {
|
||||||
@ -1013,6 +1057,10 @@ pub fn queue_mesh_view_bind_groups(
|
|||||||
);
|
);
|
||||||
entries.extend_from_slice(&env_map);
|
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 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
|
// When using WebGL, and MSAA is disabled, we can't bind the textures either
|
||||||
if cfg!(not(feature = "webgl")) {
|
if cfg!(not(feature = "webgl")) {
|
||||||
@ -1025,7 +1073,7 @@ pub fn queue_mesh_view_bind_groups(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
entries.push(BindGroupEntry {
|
entries.push(BindGroupEntry {
|
||||||
binding: 14,
|
binding: 16,
|
||||||
resource: BindingResource::TextureView(depth_view),
|
resource: BindingResource::TextureView(depth_view),
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1038,7 +1086,7 @@ pub fn queue_mesh_view_bind_groups(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
entries.push(BindGroupEntry {
|
entries.push(BindGroupEntry {
|
||||||
binding: 15,
|
binding: 17,
|
||||||
resource: BindingResource::TextureView(normal_view),
|
resource: BindingResource::TextureView(normal_view),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -53,14 +53,19 @@ var environment_map_specular: texture_cube<f32>;
|
|||||||
@group(0) @binding(13)
|
@group(0) @binding(13)
|
||||||
var environment_map_sampler: sampler;
|
var environment_map_sampler: sampler;
|
||||||
|
|
||||||
#ifdef MULTISAMPLED
|
|
||||||
@group(0) @binding(14)
|
@group(0) @binding(14)
|
||||||
var depth_prepass_texture: texture_depth_multisampled_2d;
|
var dt_lut_texture: texture_3d<f32>;
|
||||||
@group(0) @binding(15)
|
@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>;
|
var normal_prepass_texture: texture_multisampled_2d<f32>;
|
||||||
#else
|
#else
|
||||||
@group(0) @binding(14)
|
@group(0) @binding(16)
|
||||||
var depth_prepass_texture: texture_depth_2d;
|
var depth_prepass_texture: texture_depth_2d;
|
||||||
@group(0) @binding(15)
|
@group(0) @binding(17)
|
||||||
var normal_prepass_texture: texture_2d<f32>;
|
var normal_prepass_texture: texture_2d<f32>;
|
||||||
#endif
|
#endif
|
||||||
|
@ -107,11 +107,11 @@ fn fragment(in: FragmentInput) -> @location(0) vec4<f32> {
|
|||||||
#endif
|
#endif
|
||||||
#ifdef DEBAND_DITHER
|
#ifdef DEBAND_DITHER
|
||||||
var output_rgb = output_color.rgb;
|
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);
|
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
|
// 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.
|
// 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);
|
output_color = vec4(output_rgb, output_color.a);
|
||||||
#endif
|
#endif
|
||||||
#ifdef PREMULTIPLY_ALPHA
|
#ifdef PREMULTIPLY_ALPHA
|
||||||
|
@ -267,17 +267,6 @@ fn pbr(
|
|||||||
}
|
}
|
||||||
#endif // NORMAL_PREPASS
|
#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
|
#ifdef DEBAND_DITHER
|
||||||
fn dither(color: vec4<f32>, pos: vec2<f32>) -> vec4<f32> {
|
fn dither(color: vec4<f32>, pos: vec2<f32>) -> vec4<f32> {
|
||||||
return vec4<f32>(color.rgb + screen_space_dither(pos.xy), color.a);
|
return vec4<f32>(color.rgb + screen_space_dither(pos.xy), color.a);
|
||||||
|
@ -10,6 +10,7 @@ keywords = ["bevy"]
|
|||||||
|
|
||||||
[features]
|
[features]
|
||||||
png = ["image/png"]
|
png = ["image/png"]
|
||||||
|
exr = ["image/exr"]
|
||||||
hdr = ["image/hdr"]
|
hdr = ["image/hdr"]
|
||||||
tga = ["image/tga"]
|
tga = ["image/tga"]
|
||||||
jpeg = ["image/jpeg"]
|
jpeg = ["image/jpeg"]
|
||||||
|
@ -3,7 +3,7 @@ use crate::{
|
|||||||
prelude::Image,
|
prelude::Image,
|
||||||
render_asset::RenderAssets,
|
render_asset::RenderAssets,
|
||||||
render_resource::TextureView,
|
render_resource::TextureView,
|
||||||
view::{ExtractedView, ExtractedWindows, VisibleEntities},
|
view::{ColorGrading, ExtractedView, ExtractedWindows, VisibleEntities},
|
||||||
Extract,
|
Extract,
|
||||||
};
|
};
|
||||||
use bevy_asset::{AssetEvent, Assets, Handle};
|
use bevy_asset::{AssetEvent, Assets, Handle};
|
||||||
@ -530,12 +530,17 @@ pub fn extract_cameras(
|
|||||||
&CameraRenderGraph,
|
&CameraRenderGraph,
|
||||||
&GlobalTransform,
|
&GlobalTransform,
|
||||||
&VisibleEntities,
|
&VisibleEntities,
|
||||||
|
Option<&ColorGrading>,
|
||||||
)>,
|
)>,
|
||||||
>,
|
>,
|
||||||
primary_window: Extract<Query<Entity, With<PrimaryWindow>>>,
|
primary_window: Extract<Query<Entity, With<PrimaryWindow>>>,
|
||||||
) {
|
) {
|
||||||
let primary_window = primary_window.iter().next();
|
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 {
|
if !camera.is_active {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -567,6 +572,7 @@ pub fn extract_cameras(
|
|||||||
viewport_size.x,
|
viewport_size.x,
|
||||||
viewport_size.y,
|
viewport_size.y,
|
||||||
),
|
),
|
||||||
|
color_grading,
|
||||||
},
|
},
|
||||||
visible_entities.clone(),
|
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_ecs::system::{lifetimeless::SRes, Resource, SystemParamItem};
|
||||||
use bevy_math::Vec2;
|
use bevy_math::Vec2;
|
||||||
use bevy_reflect::{FromReflect, Reflect, TypeUuid};
|
use bevy_reflect::{FromReflect, Reflect, TypeUuid};
|
||||||
|
|
||||||
use std::hash::Hash;
|
use std::hash::Hash;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use wgpu::{Extent3d, TextureDimension, TextureFormat, TextureViewDescriptor};
|
use wgpu::{Extent3d, TextureDimension, TextureFormat, TextureViewDescriptor};
|
||||||
@ -33,6 +34,7 @@ pub enum ImageFormat {
|
|||||||
Dds,
|
Dds,
|
||||||
Farbfeld,
|
Farbfeld,
|
||||||
Gif,
|
Gif,
|
||||||
|
OpenExr,
|
||||||
Hdr,
|
Hdr,
|
||||||
Ico,
|
Ico,
|
||||||
Jpeg,
|
Jpeg,
|
||||||
@ -52,6 +54,7 @@ impl ImageFormat {
|
|||||||
"image/jpeg" => ImageFormat::Jpeg,
|
"image/jpeg" => ImageFormat::Jpeg,
|
||||||
"image/ktx2" => ImageFormat::Ktx2,
|
"image/ktx2" => ImageFormat::Ktx2,
|
||||||
"image/png" => ImageFormat::Png,
|
"image/png" => ImageFormat::Png,
|
||||||
|
"image/x-exr" => ImageFormat::OpenExr,
|
||||||
"image/x-targa" | "image/x-tga" => ImageFormat::Tga,
|
"image/x-targa" | "image/x-tga" => ImageFormat::Tga,
|
||||||
_ => return None,
|
_ => return None,
|
||||||
})
|
})
|
||||||
@ -65,6 +68,7 @@ impl ImageFormat {
|
|||||||
"dds" => ImageFormat::Dds,
|
"dds" => ImageFormat::Dds,
|
||||||
"ff" | "farbfeld" => ImageFormat::Farbfeld,
|
"ff" | "farbfeld" => ImageFormat::Farbfeld,
|
||||||
"gif" => ImageFormat::Gif,
|
"gif" => ImageFormat::Gif,
|
||||||
|
"exr" => ImageFormat::OpenExr,
|
||||||
"hdr" => ImageFormat::Hdr,
|
"hdr" => ImageFormat::Hdr,
|
||||||
"ico" => ImageFormat::Ico,
|
"ico" => ImageFormat::Ico,
|
||||||
"jpg" | "jpeg" => ImageFormat::Jpeg,
|
"jpg" | "jpeg" => ImageFormat::Jpeg,
|
||||||
@ -85,6 +89,7 @@ impl ImageFormat {
|
|||||||
ImageFormat::Dds => image::ImageFormat::Dds,
|
ImageFormat::Dds => image::ImageFormat::Dds,
|
||||||
ImageFormat::Farbfeld => image::ImageFormat::Farbfeld,
|
ImageFormat::Farbfeld => image::ImageFormat::Farbfeld,
|
||||||
ImageFormat::Gif => image::ImageFormat::Gif,
|
ImageFormat::Gif => image::ImageFormat::Gif,
|
||||||
|
ImageFormat::OpenExr => image::ImageFormat::OpenExr,
|
||||||
ImageFormat::Hdr => image::ImageFormat::Hdr,
|
ImageFormat::Hdr => image::ImageFormat::Hdr,
|
||||||
ImageFormat::Ico => image::ImageFormat::Ico,
|
ImageFormat::Ico => image::ImageFormat::Ico,
|
||||||
ImageFormat::Jpeg => image::ImageFormat::Jpeg,
|
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];
|
let mut wgpu_data = vec![Vec::default(); (layer_count * face_count) as usize];
|
||||||
for (level, level_data) in levels.iter().enumerate() {
|
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),
|
(width as usize >> level).max(1),
|
||||||
(height as usize >> level).max(1),
|
(height as usize >> level).max(1),
|
||||||
|
(depth as usize >> level).max(1),
|
||||||
);
|
);
|
||||||
let (num_blocks_x, num_blocks_y) = (
|
let (num_blocks_x, num_blocks_y) = (
|
||||||
((level_width + block_width_pixels - 1) / block_width_pixels).max(1),
|
((level_width + block_width_pixels - 1) / block_width_pixels).max(1),
|
||||||
((level_height + block_height_pixels - 1) / block_height_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;
|
let mut index = 0;
|
||||||
for _layer in 0..layer_count {
|
for _layer in 0..layer_count {
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
mod basis;
|
mod basis;
|
||||||
#[cfg(feature = "dds")]
|
#[cfg(feature = "dds")]
|
||||||
mod dds;
|
mod dds;
|
||||||
|
#[cfg(feature = "exr")]
|
||||||
|
mod exr_texture_loader;
|
||||||
mod fallback_image;
|
mod fallback_image;
|
||||||
#[cfg(feature = "hdr")]
|
#[cfg(feature = "hdr")]
|
||||||
mod hdr_texture_loader;
|
mod hdr_texture_loader;
|
||||||
@ -19,6 +21,8 @@ pub use self::image::*;
|
|||||||
pub use self::ktx2::*;
|
pub use self::ktx2::*;
|
||||||
#[cfg(feature = "dds")]
|
#[cfg(feature = "dds")]
|
||||||
pub use dds::*;
|
pub use dds::*;
|
||||||
|
#[cfg(feature = "exr")]
|
||||||
|
pub use exr_texture_loader::*;
|
||||||
#[cfg(feature = "hdr")]
|
#[cfg(feature = "hdr")]
|
||||||
pub use hdr_texture_loader::*;
|
pub use hdr_texture_loader::*;
|
||||||
|
|
||||||
@ -79,6 +83,11 @@ impl Plugin for ImagePlugin {
|
|||||||
app.init_asset_loader::<ImageTextureLoader>();
|
app.init_asset_loader::<ImageTextureLoader>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "exr")]
|
||||||
|
{
|
||||||
|
app.init_asset_loader::<ExrTextureLoader>();
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "hdr")]
|
#[cfg(feature = "hdr")]
|
||||||
{
|
{
|
||||||
app.init_asset_loader::<HdrTextureLoader>();
|
app.init_asset_loader::<HdrTextureLoader>();
|
||||||
|
@ -106,6 +106,7 @@ pub struct ExtractedView {
|
|||||||
pub hdr: bool,
|
pub hdr: bool,
|
||||||
// uvec4(origin.x, origin.y, width, height)
|
// uvec4(origin.x, origin.y, width, height)
|
||||||
pub viewport: UVec4,
|
pub viewport: UVec4,
|
||||||
|
pub color_grading: ColorGrading,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ExtractedView {
|
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)]
|
#[derive(Clone, ShaderType)]
|
||||||
pub struct ViewUniform {
|
pub struct ViewUniform {
|
||||||
view_proj: Mat4,
|
view_proj: Mat4,
|
||||||
@ -126,6 +161,7 @@ pub struct ViewUniform {
|
|||||||
world_position: Vec3,
|
world_position: Vec3,
|
||||||
// viewport(x_origin, y_origin, width, height)
|
// viewport(x_origin, y_origin, width, height)
|
||||||
viewport: Vec4,
|
viewport: Vec4,
|
||||||
|
color_grading: ColorGrading,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Resource, Default)]
|
#[derive(Resource, Default)]
|
||||||
@ -287,6 +323,7 @@ fn prepare_view_uniforms(
|
|||||||
inverse_projection,
|
inverse_projection,
|
||||||
world_position: camera.transform.translation(),
|
world_position: camera.transform.translation(),
|
||||||
viewport: camera.viewport.as_vec4(),
|
viewport: camera.viewport.as_vec4(),
|
||||||
|
color_grading: camera.color_grading,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,5 +1,12 @@
|
|||||||
#define_import_path bevy_render::view
|
#define_import_path bevy_render::view
|
||||||
|
|
||||||
|
struct ColorGrading {
|
||||||
|
exposure: f32,
|
||||||
|
gamma: f32,
|
||||||
|
pre_saturation: f32,
|
||||||
|
post_saturation: f32,
|
||||||
|
}
|
||||||
|
|
||||||
struct View {
|
struct View {
|
||||||
view_proj: mat4x4<f32>,
|
view_proj: mat4x4<f32>,
|
||||||
inverse_view_proj: mat4x4<f32>,
|
inverse_view_proj: mat4x4<f32>,
|
||||||
@ -10,4 +17,5 @@ struct View {
|
|||||||
world_position: vec3<f32>,
|
world_position: vec3<f32>,
|
||||||
// viewport(x_origin, y_origin, width, height)
|
// viewport(x_origin, y_origin, width, height)
|
||||||
viewport: vec4<f32>,
|
viewport: vec4<f32>,
|
||||||
|
color_grading: ColorGrading,
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
#import bevy_sprite::mesh2d_types
|
#import bevy_sprite::mesh2d_types
|
||||||
#import bevy_sprite::mesh2d_view_bindings
|
#import bevy_sprite::mesh2d_view_bindings
|
||||||
|
|
||||||
|
#ifdef TONEMAP_IN_SHADER
|
||||||
|
#import bevy_core_pipeline::tonemapping
|
||||||
|
#endif
|
||||||
|
|
||||||
struct ColorMaterial {
|
struct ColorMaterial {
|
||||||
color: vec4<f32>,
|
color: vec4<f32>,
|
||||||
// 'flags' is a bit field indicating various options. u32 is 32 bits so we have up to 32 options.
|
// '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) {
|
if ((material.flags & COLOR_MATERIAL_FLAGS_TEXTURE_BIT) != 0u) {
|
||||||
output_color = output_color * textureSample(texture, texture_sampler, in.uv);
|
output_color = output_color * textureSample(texture, texture_sampler, in.uv);
|
||||||
}
|
}
|
||||||
|
#ifdef TONEMAP_IN_SHADER
|
||||||
|
output_color = tone_mapping(output_color);
|
||||||
|
#endif
|
||||||
return output_color;
|
return output_color;
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
use bevy_app::{App, Plugin};
|
use bevy_app::{App, Plugin};
|
||||||
use bevy_asset::{AddAsset, AssetEvent, AssetServer, Assets, Handle};
|
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_derive::{Deref, DerefMut};
|
||||||
use bevy_ecs::{
|
use bevy_ecs::{
|
||||||
prelude::*,
|
prelude::*,
|
||||||
@ -327,6 +330,7 @@ pub fn queue_material2d_meshes<M: Material2d>(
|
|||||||
&ExtractedView,
|
&ExtractedView,
|
||||||
&VisibleEntities,
|
&VisibleEntities,
|
||||||
Option<&Tonemapping>,
|
Option<&Tonemapping>,
|
||||||
|
Option<&DebandDither>,
|
||||||
&mut RenderPhase<Transparent2d>,
|
&mut RenderPhase<Transparent2d>,
|
||||||
)>,
|
)>,
|
||||||
) where
|
) where
|
||||||
@ -336,19 +340,32 @@ pub fn queue_material2d_meshes<M: Material2d>(
|
|||||||
return;
|
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 draw_transparent_pbr = transparent_draw_functions.read().id::<DrawMaterial2d<M>>();
|
||||||
|
|
||||||
let mut view_key = Mesh2dPipelineKey::from_msaa_samples(msaa.samples())
|
let mut view_key = Mesh2dPipelineKey::from_msaa_samples(msaa.samples())
|
||||||
| Mesh2dPipelineKey::from_hdr(view.hdr);
|
| 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;
|
view_key |= Mesh2dPipelineKey::TONEMAP_IN_SHADER;
|
||||||
|
view_key |= match tonemapping {
|
||||||
if *deband_dither {
|
Tonemapping::None => Mesh2dPipelineKey::TONEMAP_METHOD_NONE,
|
||||||
view_key |= Mesh2dPipelineKey::DEBAND_DITHER;
|
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_app::Plugin;
|
||||||
use bevy_asset::{load_internal_asset, Handle, HandleUntyped};
|
use bevy_asset::{load_internal_asset, Handle, HandleUntyped};
|
||||||
|
|
||||||
use bevy_ecs::{
|
use bevy_ecs::{
|
||||||
prelude::*,
|
prelude::*,
|
||||||
query::ROQueryItem,
|
query::ROQueryItem,
|
||||||
@ -292,6 +293,15 @@ bitflags::bitflags! {
|
|||||||
const DEBAND_DITHER = (1 << 2);
|
const DEBAND_DITHER = (1 << 2);
|
||||||
const MSAA_RESERVED_BITS = Self::MSAA_MASK_BITS << Self::MSAA_SHIFT_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 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 MSAA_SHIFT_BITS: u32 = 32 - Self::MSAA_MASK_BITS.count_ones();
|
||||||
const PRIMITIVE_TOPOLOGY_MASK_BITS: u32 = 0b111;
|
const PRIMITIVE_TOPOLOGY_MASK_BITS: u32 = 0b111;
|
||||||
const PRIMITIVE_TOPOLOGY_SHIFT_BITS: u32 = Self::MSAA_SHIFT_BITS - 3;
|
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 {
|
pub fn from_msaa_samples(msaa_samples: u32) -> Self {
|
||||||
let msaa_bits =
|
let msaa_bits =
|
||||||
@ -379,6 +392,27 @@ impl SpecializedMeshPipeline for Mesh2dPipeline {
|
|||||||
if key.contains(Mesh2dPipelineKey::TONEMAP_IN_SHADER) {
|
if key.contains(Mesh2dPipelineKey::TONEMAP_IN_SHADER) {
|
||||||
shader_defs.push("TONEMAP_IN_SHADER".into());
|
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.
|
// Debanding is tied to tonemapping in the shader, cannot run without it.
|
||||||
if key.contains(Mesh2dPipelineKey::DEBAND_DITHER) {
|
if key.contains(Mesh2dPipelineKey::DEBAND_DITHER) {
|
||||||
shader_defs.push("DEBAND_DITHER".into());
|
shader_defs.push("DEBAND_DITHER".into());
|
||||||
|
@ -4,6 +4,10 @@
|
|||||||
// NOTE: Bindings must come before functions that use them!
|
// NOTE: Bindings must come before functions that use them!
|
||||||
#import bevy_sprite::mesh2d_functions
|
#import bevy_sprite::mesh2d_functions
|
||||||
|
|
||||||
|
#ifdef TONEMAP_IN_SHADER
|
||||||
|
#import bevy_core_pipeline::tonemapping
|
||||||
|
#endif
|
||||||
|
|
||||||
struct Vertex {
|
struct Vertex {
|
||||||
#ifdef VERTEX_POSITIONS
|
#ifdef VERTEX_POSITIONS
|
||||||
@location(0) position: vec3<f32>,
|
@location(0) position: vec3<f32>,
|
||||||
@ -61,7 +65,11 @@ struct FragmentInput {
|
|||||||
@fragment
|
@fragment
|
||||||
fn fragment(in: FragmentInput) -> @location(0) vec4<f32> {
|
fn fragment(in: FragmentInput) -> @location(0) vec4<f32> {
|
||||||
#ifdef VERTEX_COLORS
|
#ifdef VERTEX_COLORS
|
||||||
return in.color;
|
var color = in.color;
|
||||||
|
#ifdef TONEMAP_IN_SHADER
|
||||||
|
color = tone_mapping(color);
|
||||||
|
#endif
|
||||||
|
return color;
|
||||||
#else
|
#else
|
||||||
return vec4<f32>(1.0, 0.0, 1.0, 1.0);
|
return vec4<f32>(1.0, 0.0, 1.0, 1.0);
|
||||||
#endif
|
#endif
|
||||||
|
@ -5,7 +5,10 @@ use crate::{
|
|||||||
Sprite, SPRITE_SHADER_HANDLE,
|
Sprite, SPRITE_SHADER_HANDLE,
|
||||||
};
|
};
|
||||||
use bevy_asset::{AssetEvent, Assets, Handle, HandleId};
|
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::{
|
use bevy_ecs::{
|
||||||
prelude::*,
|
prelude::*,
|
||||||
system::{lifetimeless::*, SystemParamItem, SystemState},
|
system::{lifetimeless::*, SystemParamItem, SystemState},
|
||||||
@ -153,12 +156,24 @@ bitflags::bitflags! {
|
|||||||
const TONEMAP_IN_SHADER = (1 << 2);
|
const TONEMAP_IN_SHADER = (1 << 2);
|
||||||
const DEBAND_DITHER = (1 << 3);
|
const DEBAND_DITHER = (1 << 3);
|
||||||
const MSAA_RESERVED_BITS = Self::MSAA_MASK_BITS << Self::MSAA_SHIFT_BITS;
|
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 {
|
impl SpritePipelineKey {
|
||||||
const MSAA_MASK_BITS: u32 = 0b111;
|
const MSAA_MASK_BITS: u32 = 0b111;
|
||||||
const MSAA_SHIFT_BITS: u32 = 32 - Self::MSAA_MASK_BITS.count_ones();
|
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]
|
#[inline]
|
||||||
pub const fn from_msaa_samples(msaa_samples: u32) -> Self {
|
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) {
|
if key.contains(SpritePipelineKey::TONEMAP_IN_SHADER) {
|
||||||
shader_defs.push("TONEMAP_IN_SHADER".into());
|
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.
|
// Debanding is tied to tonemapping in the shader, cannot run without it.
|
||||||
if key.contains(SpritePipelineKey::DEBAND_DITHER) {
|
if key.contains(SpritePipelineKey::DEBAND_DITHER) {
|
||||||
shader_defs.push("DEBAND_DITHER".into());
|
shader_defs.push("DEBAND_DITHER".into());
|
||||||
@ -462,6 +498,7 @@ pub fn queue_sprites(
|
|||||||
&VisibleEntities,
|
&VisibleEntities,
|
||||||
&ExtractedView,
|
&ExtractedView,
|
||||||
Option<&Tonemapping>,
|
Option<&Tonemapping>,
|
||||||
|
Option<&DebandDither>,
|
||||||
)>,
|
)>,
|
||||||
events: Res<SpriteAssetEvents>,
|
events: Res<SpriteAssetEvents>,
|
||||||
) {
|
) {
|
||||||
@ -517,17 +554,36 @@ pub fn queue_sprites(
|
|||||||
});
|
});
|
||||||
let image_bind_groups = &mut *image_bind_groups;
|
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;
|
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 {
|
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;
|
view_key |= SpritePipelineKey::DEBAND_DITHER;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
let pipeline = pipelines.specialize(
|
let pipeline = pipelines.specialize(
|
||||||
&pipeline_cache,
|
&pipeline_cache,
|
||||||
&sprite_pipeline,
|
&sprite_pipeline,
|
||||||
|
@ -45,7 +45,7 @@ fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef TONEMAP_IN_SHADER
|
#ifdef TONEMAP_IN_SHADER
|
||||||
color = vec4<f32>(reinhard_luminance(color.rgb), color.a);
|
color = tone_mapping(color);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
return color;
|
return color;
|
||||||
|
@ -277,6 +277,7 @@ pub fn extract_default_ui_camera_view<T: Component>(
|
|||||||
physical_size.x,
|
physical_size.x,
|
||||||
physical_size.y,
|
physical_size.y,
|
||||||
),
|
),
|
||||||
|
color_grading: Default::default(),
|
||||||
})
|
})
|
||||||
.id();
|
.id();
|
||||||
commands.get_or_spawn(entity).insert((
|
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"
|
[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
|
[Spotlight](../examples/3d/spotlight.rs) | Illustrates spot lights
|
||||||
[Texture](../examples/3d/texture.rs) | Shows configuration of texture materials
|
[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
|
[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
|
[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
|
[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 New Issue
Block a user