mirror of
https://github.com/bevyengine/bevy
synced 2024-11-24 05:33:04 +00:00
Implement Auto Exposure plugin (#12792)
# Objective - Add auto exposure/eye adaptation to the bevy render pipeline. - Support features that users might expect from other engines: - Metering masks - Compensation curves - Smooth exposure transitions This PR is based on an implementation I already built for a personal project before https://github.com/bevyengine/bevy/pull/8809 was submitted, so I wasn't able to adopt that PR in the proper way. I've still drawn inspiration from it, so @fintelia should be credited as well. ## Solution An auto exposure compute shader builds a 64 bin histogram of the scene's luminance, and then adjusts the exposure based on that histogram. Using a histogram allows the system to ignore outliers like shadows and specular highlights, and it allows to give more weight to certain areas based on a mask. --- ## Changelog - Added: AutoExposure plugin that allows to adjust a camera's exposure based on it's scene's luminance. --------- Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
This commit is contained in:
parent
bdb4899978
commit
d390420093
18 changed files with 1282 additions and 13 deletions
11
Cargo.toml
11
Cargo.toml
|
@ -677,6 +677,17 @@ description = "A scene showcasing the distance fog effect"
|
|||
category = "3D Rendering"
|
||||
wasm = true
|
||||
|
||||
[[example]]
|
||||
name = "auto_exposure"
|
||||
path = "examples/3d/auto_exposure.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[package.metadata.example.auto_exposure]
|
||||
name = "Auto Exposure"
|
||||
description = "A scene showcasing auto exposure"
|
||||
category = "3D Rendering"
|
||||
wasm = false
|
||||
|
||||
[[example]]
|
||||
name = "blend_modes"
|
||||
path = "examples/3d/blend_modes.rs"
|
||||
|
|
BIN
assets/textures/basic_metering_mask.png
Normal file
BIN
assets/textures/basic_metering_mask.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 447 B |
|
@ -37,6 +37,7 @@ serde = { version = "1", features = ["derive"] }
|
|||
bitflags = "2.3"
|
||||
radsort = "0.1"
|
||||
nonmax = "0.5"
|
||||
thiserror = "1.0"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
|
191
crates/bevy_core_pipeline/src/auto_exposure/auto_exposure.wgsl
Normal file
191
crates/bevy_core_pipeline/src/auto_exposure/auto_exposure.wgsl
Normal file
|
@ -0,0 +1,191 @@
|
|||
// Auto exposure
|
||||
//
|
||||
// This shader computes an auto exposure value for the current frame,
|
||||
// which is then used as an exposure correction in the tone mapping shader.
|
||||
//
|
||||
// The auto exposure value is computed in two passes:
|
||||
// * The compute_histogram pass calculates a histogram of the luminance values in the scene,
|
||||
// taking into account the metering mask texture. The metering mask is a grayscale texture
|
||||
// that defines the areas of the screen that should be given more weight when calculating
|
||||
// the average luminance value. For example, the middle area of the screen might be more important
|
||||
// than the edges.
|
||||
// * The compute_average pass calculates the average luminance value of the scene, taking
|
||||
// into account the low_percent and high_percent settings. These settings define the
|
||||
// percentage of the histogram that should be excluded when calculating the average. This
|
||||
// is useful to avoid overexposure when you have a lot of shadows, or underexposure when you
|
||||
// have a lot of bright specular reflections.
|
||||
//
|
||||
// The final target_exposure is finally used to smoothly adjust the exposure value over time.
|
||||
|
||||
#import bevy_render::view::View
|
||||
#import bevy_render::globals::Globals
|
||||
|
||||
// Constant to convert RGB to luminance, taken from Real Time Rendering, Vol 4 pg. 278, 4th edition
|
||||
const RGB_TO_LUM = vec3<f32>(0.2125, 0.7154, 0.0721);
|
||||
|
||||
struct AutoExposure {
|
||||
min_log_lum: f32,
|
||||
inv_log_lum_range: f32,
|
||||
log_lum_range: f32,
|
||||
low_percent: f32,
|
||||
high_percent: f32,
|
||||
speed_up: f32,
|
||||
speed_down: f32,
|
||||
exponential_transition_distance: f32,
|
||||
}
|
||||
|
||||
struct CompensationCurve {
|
||||
min_log_lum: f32,
|
||||
inv_log_lum_range: f32,
|
||||
min_compensation: f32,
|
||||
compensation_range: f32,
|
||||
}
|
||||
|
||||
@group(0) @binding(0) var<uniform> globals: Globals;
|
||||
|
||||
@group(0) @binding(1) var<uniform> settings: AutoExposure;
|
||||
|
||||
@group(0) @binding(2) var tex_color: texture_2d<f32>;
|
||||
|
||||
@group(0) @binding(3) var tex_mask: texture_2d<f32>;
|
||||
|
||||
@group(0) @binding(4) var tex_compensation: texture_1d<f32>;
|
||||
|
||||
@group(0) @binding(5) var<uniform> compensation_curve: CompensationCurve;
|
||||
|
||||
@group(0) @binding(6) var<storage, read_write> histogram: array<atomic<u32>, 64>;
|
||||
|
||||
@group(0) @binding(7) var<storage, read_write> exposure: f32;
|
||||
|
||||
@group(0) @binding(8) var<storage, read_write> view: View;
|
||||
|
||||
var<workgroup> histogram_shared: array<atomic<u32>, 64>;
|
||||
|
||||
// For a given color, return the histogram bin index
|
||||
fn color_to_bin(hdr: vec3<f32>) -> u32 {
|
||||
// Convert color to luminance
|
||||
let lum = dot(hdr, RGB_TO_LUM);
|
||||
|
||||
if lum < exp2(settings.min_log_lum) {
|
||||
return 0u;
|
||||
}
|
||||
|
||||
// Calculate the log_2 luminance and express it as a value in [0.0, 1.0]
|
||||
// where 0.0 represents the minimum luminance, and 1.0 represents the max.
|
||||
let log_lum = saturate((log2(lum) - settings.min_log_lum) * settings.inv_log_lum_range);
|
||||
|
||||
// Map [0, 1] to [1, 63]. The zeroth bin is handled by the epsilon check above.
|
||||
return u32(log_lum * 62.0 + 1.0);
|
||||
}
|
||||
|
||||
// Read the metering mask at the given UV coordinates, returning a weight for the histogram.
|
||||
//
|
||||
// Since the histogram is summed in the compute_average step, there is a limit to the amount of
|
||||
// distinct values that can be represented. When using the chosen value of 16, the maximum
|
||||
// amount of pixels that can be weighted and summed is 2^32 / 16 = 16384^2.
|
||||
fn metering_weight(coords: vec2<f32>) -> u32 {
|
||||
let pos = vec2<i32>(coords * vec2<f32>(textureDimensions(tex_mask)));
|
||||
let mask = textureLoad(tex_mask, pos, 0).r;
|
||||
return u32(mask * 16.0);
|
||||
}
|
||||
|
||||
@compute @workgroup_size(16, 16, 1)
|
||||
fn compute_histogram(
|
||||
@builtin(global_invocation_id) global_invocation_id: vec3<u32>,
|
||||
@builtin(local_invocation_index) local_invocation_index: u32
|
||||
) {
|
||||
// Clear the workgroup shared histogram
|
||||
histogram_shared[local_invocation_index] = 0u;
|
||||
|
||||
// Wait for all workgroup threads to clear the shared histogram
|
||||
storageBarrier();
|
||||
|
||||
let dim = vec2<u32>(textureDimensions(tex_color));
|
||||
let uv = vec2<f32>(global_invocation_id.xy) / vec2<f32>(dim);
|
||||
|
||||
if global_invocation_id.x < dim.x && global_invocation_id.y < dim.y {
|
||||
let col = textureLoad(tex_color, vec2<i32>(global_invocation_id.xy), 0).rgb;
|
||||
let index = color_to_bin(col);
|
||||
let weight = metering_weight(uv);
|
||||
|
||||
// Increment the shared histogram bin by the weight obtained from the metering mask
|
||||
atomicAdd(&histogram_shared[index], weight);
|
||||
}
|
||||
|
||||
// Wait for all workgroup threads to finish updating the workgroup histogram
|
||||
workgroupBarrier();
|
||||
|
||||
// Accumulate the workgroup histogram into the global histogram.
|
||||
// Note that the global histogram was not cleared at the beginning,
|
||||
// as it will be cleared in compute_average.
|
||||
atomicAdd(&histogram[local_invocation_index], histogram_shared[local_invocation_index]);
|
||||
}
|
||||
|
||||
@compute @workgroup_size(1, 1, 1)
|
||||
fn compute_average(@builtin(local_invocation_index) local_index: u32) {
|
||||
var histogram_sum = 0u;
|
||||
|
||||
// Calculate the cumulative histogram and clear the histogram bins.
|
||||
// Each bin in the cumulative histogram contains the sum of all bins up to that point.
|
||||
// This way we can quickly exclude the portion of lowest and highest samples as required by
|
||||
// the low_percent and high_percent settings.
|
||||
for (var i=0u; i<64u; i+=1u) {
|
||||
histogram_sum += histogram[i];
|
||||
histogram_shared[i] = histogram_sum;
|
||||
|
||||
// Clear the histogram bin for the next frame
|
||||
histogram[i] = 0u;
|
||||
}
|
||||
|
||||
let first_index = u32(f32(histogram_sum) * settings.low_percent);
|
||||
let last_index = u32(f32(histogram_sum) * settings.high_percent);
|
||||
|
||||
var count = 0u;
|
||||
var sum = 0.0;
|
||||
for (var i=1u; i<64u; i+=1u) {
|
||||
// The number of pixels in the bin. The histogram values are clamped to
|
||||
// first_index and last_index to exclude the lowest and highest samples.
|
||||
let bin_count =
|
||||
clamp(histogram_shared[i], first_index, last_index) -
|
||||
clamp(histogram_shared[i - 1u], first_index, last_index);
|
||||
|
||||
sum += f32(bin_count) * f32(i);
|
||||
count += bin_count;
|
||||
}
|
||||
|
||||
var target_exposure = 0.0;
|
||||
|
||||
if count > 0u {
|
||||
// The average luminance of the included histogram samples.
|
||||
let avg_lum = sum / (f32(count) * 63.0)
|
||||
* settings.log_lum_range
|
||||
+ settings.min_log_lum;
|
||||
|
||||
// The position in the compensation curve texture to sample for avg_lum.
|
||||
let u = (avg_lum - compensation_curve.min_log_lum) * compensation_curve.inv_log_lum_range;
|
||||
|
||||
// The target exposure is the negative of the average log luminance.
|
||||
// The compensation value is added to the target exposure to adjust the exposure for
|
||||
// artistic purposes.
|
||||
target_exposure = textureLoad(tex_compensation, i32(saturate(u) * 255.0), 0).r
|
||||
* compensation_curve.compensation_range
|
||||
+ compensation_curve.min_compensation
|
||||
- avg_lum;
|
||||
}
|
||||
|
||||
// Smoothly adjust the `exposure` towards the `target_exposure`
|
||||
let delta = target_exposure - exposure;
|
||||
if target_exposure > exposure {
|
||||
let speed_down = settings.speed_down * globals.delta_time;
|
||||
let exp_down = speed_down / settings.exponential_transition_distance;
|
||||
exposure = exposure + min(speed_down, delta * exp_down);
|
||||
} else {
|
||||
let speed_up = settings.speed_up * globals.delta_time;
|
||||
let exp_up = speed_up / settings.exponential_transition_distance;
|
||||
exposure = exposure + max(-speed_up, delta * exp_up);
|
||||
}
|
||||
|
||||
// Apply the exposure to the color grading settings, from where it will be used for the color
|
||||
// grading pass.
|
||||
view.color_grading.exposure += exposure;
|
||||
}
|
87
crates/bevy_core_pipeline/src/auto_exposure/buffers.rs
Normal file
87
crates/bevy_core_pipeline/src/auto_exposure/buffers.rs
Normal file
|
@ -0,0 +1,87 @@
|
|||
use bevy_ecs::prelude::*;
|
||||
use bevy_render::{
|
||||
render_resource::{StorageBuffer, UniformBuffer},
|
||||
renderer::{RenderDevice, RenderQueue},
|
||||
Extract,
|
||||
};
|
||||
use bevy_utils::{Entry, HashMap};
|
||||
|
||||
use super::pipeline::AutoExposureSettingsUniform;
|
||||
use super::AutoExposureSettings;
|
||||
|
||||
#[derive(Resource, Default)]
|
||||
pub(super) struct AutoExposureBuffers {
|
||||
pub(super) buffers: HashMap<Entity, AutoExposureBuffer>,
|
||||
}
|
||||
|
||||
pub(super) struct AutoExposureBuffer {
|
||||
pub(super) state: StorageBuffer<f32>,
|
||||
pub(super) settings: UniformBuffer<AutoExposureSettingsUniform>,
|
||||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
pub(super) struct ExtractedStateBuffers {
|
||||
changed: Vec<(Entity, AutoExposureSettings)>,
|
||||
removed: Vec<Entity>,
|
||||
}
|
||||
|
||||
pub(super) fn extract_buffers(
|
||||
mut commands: Commands,
|
||||
changed: Extract<Query<(Entity, &AutoExposureSettings), Changed<AutoExposureSettings>>>,
|
||||
mut removed: Extract<RemovedComponents<AutoExposureSettings>>,
|
||||
) {
|
||||
commands.insert_resource(ExtractedStateBuffers {
|
||||
changed: changed
|
||||
.iter()
|
||||
.map(|(entity, settings)| (entity, settings.clone()))
|
||||
.collect(),
|
||||
removed: removed.read().collect(),
|
||||
});
|
||||
}
|
||||
|
||||
pub(super) fn prepare_buffers(
|
||||
device: Res<RenderDevice>,
|
||||
queue: Res<RenderQueue>,
|
||||
mut extracted: ResMut<ExtractedStateBuffers>,
|
||||
mut buffers: ResMut<AutoExposureBuffers>,
|
||||
) {
|
||||
for (entity, settings) in extracted.changed.drain(..) {
|
||||
let (min_log_lum, max_log_lum) = settings.range.into_inner();
|
||||
let (low_percent, high_percent) = settings.filter.into_inner();
|
||||
let initial_state = 0.0f32.clamp(min_log_lum, max_log_lum);
|
||||
|
||||
let settings = AutoExposureSettingsUniform {
|
||||
min_log_lum,
|
||||
inv_log_lum_range: 1.0 / (max_log_lum - min_log_lum),
|
||||
log_lum_range: max_log_lum - min_log_lum,
|
||||
low_percent,
|
||||
high_percent,
|
||||
speed_up: settings.speed_brighten,
|
||||
speed_down: settings.speed_darken,
|
||||
exponential_transition_distance: settings.exponential_transition_distance,
|
||||
};
|
||||
|
||||
match buffers.buffers.entry(entity) {
|
||||
Entry::Occupied(mut entry) => {
|
||||
// Update the settings buffer, but skip updating the state buffer.
|
||||
// The state buffer is skipped so that the animation stays continuous.
|
||||
let value = entry.get_mut();
|
||||
value.settings.set(settings);
|
||||
value.settings.write_buffer(&device, &queue);
|
||||
}
|
||||
Entry::Vacant(entry) => {
|
||||
let value = entry.insert(AutoExposureBuffer {
|
||||
state: StorageBuffer::from(initial_state),
|
||||
settings: UniformBuffer::from(settings),
|
||||
});
|
||||
|
||||
value.state.write_buffer(&device, &queue);
|
||||
value.settings.write_buffer(&device, &queue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for entity in extracted.removed.drain(..) {
|
||||
buffers.buffers.remove(&entity);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,226 @@
|
|||
use bevy_asset::prelude::*;
|
||||
use bevy_ecs::system::{lifetimeless::SRes, SystemParamItem};
|
||||
use bevy_math::{cubic_splines::CubicGenerator, FloatExt, Vec2};
|
||||
use bevy_reflect::prelude::*;
|
||||
use bevy_render::{
|
||||
render_asset::{RenderAsset, RenderAssetUsages},
|
||||
render_resource::{
|
||||
Extent3d, ShaderType, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages,
|
||||
TextureView, UniformBuffer,
|
||||
},
|
||||
renderer::{RenderDevice, RenderQueue},
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
const LUT_SIZE: usize = 256;
|
||||
|
||||
/// An auto exposure compensation curve.
|
||||
/// This curve is used to map the average log luminance of a scene to an
|
||||
/// exposure compensation value, to allow for fine control over the final exposure.
|
||||
#[derive(Asset, Reflect, Debug, Clone)]
|
||||
#[reflect(Default)]
|
||||
pub struct AutoExposureCompensationCurve {
|
||||
/// The minimum log luminance value in the curve. (the x-axis)
|
||||
min_log_lum: f32,
|
||||
/// The maximum log luminance value in the curve. (the x-axis)
|
||||
max_log_lum: f32,
|
||||
/// The minimum exposure compensation value in the curve. (the y-axis)
|
||||
min_compensation: f32,
|
||||
/// The maximum exposure compensation value in the curve. (the y-axis)
|
||||
max_compensation: f32,
|
||||
/// The lookup table for the curve. Uploaded to the GPU as a 1D texture.
|
||||
/// Each value in the LUT is a `u8` representing a normalized exposure compensation value:
|
||||
/// * `0` maps to `min_compensation`
|
||||
/// * `255` maps to `max_compensation`
|
||||
/// The position in the LUT corresponds to the normalized log luminance value.
|
||||
/// * `0` maps to `min_log_lum`
|
||||
/// * `LUT_SIZE - 1` maps to `max_log_lum`
|
||||
lut: [u8; LUT_SIZE],
|
||||
}
|
||||
|
||||
/// Various errors that can occur when constructing an [`AutoExposureCompensationCurve`].
|
||||
#[derive(Error, Debug)]
|
||||
pub enum AutoExposureCompensationCurveError {
|
||||
/// A discontinuity was found in the curve.
|
||||
#[error("discontinuity found between curve segments")]
|
||||
DiscontinuityFound,
|
||||
/// The curve is not monotonically increasing on the x-axis.
|
||||
#[error("curve is not monotonically increasing on the x-axis")]
|
||||
NotMonotonic,
|
||||
}
|
||||
|
||||
impl Default for AutoExposureCompensationCurve {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
min_log_lum: 0.0,
|
||||
max_log_lum: 0.0,
|
||||
min_compensation: 0.0,
|
||||
max_compensation: 0.0,
|
||||
lut: [0; LUT_SIZE],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AutoExposureCompensationCurve {
|
||||
const SAMPLES_PER_SEGMENT: usize = 64;
|
||||
|
||||
/// Build an [`AutoExposureCompensationCurve`] from a [`CubicGenerator<Vec2>`], where:
|
||||
/// - x represents the average log luminance of the scene in EV-100;
|
||||
/// - y represents the exposure compensation value in F-stops.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// If the curve is not monotonically increasing on the x-axis,
|
||||
/// returns [`AutoExposureCompensationCurveError::NotMonotonic`].
|
||||
///
|
||||
/// If a discontinuity is found between curve segments,
|
||||
/// returns [`AutoExposureCompensationCurveError::DiscontinuityFound`].
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use bevy_asset::prelude::*;
|
||||
/// # use bevy_math::vec2;
|
||||
/// # use bevy_math::cubic_splines::*;
|
||||
/// # use bevy_core_pipeline::auto_exposure::AutoExposureCompensationCurve;
|
||||
/// # let mut compensation_curves = Assets::<AutoExposureCompensationCurve>::default();
|
||||
/// let curve: Handle<AutoExposureCompensationCurve> = compensation_curves.add(
|
||||
/// AutoExposureCompensationCurve::from_curve(LinearSpline::new([
|
||||
/// vec2(-4.0, -2.0),
|
||||
/// vec2(0.0, 0.0),
|
||||
/// vec2(2.0, 0.0),
|
||||
/// vec2(4.0, 2.0),
|
||||
/// ]))
|
||||
/// .unwrap()
|
||||
/// );
|
||||
/// ```
|
||||
pub fn from_curve<T>(curve: T) -> Result<Self, AutoExposureCompensationCurveError>
|
||||
where
|
||||
T: CubicGenerator<Vec2>,
|
||||
{
|
||||
let curve = curve.to_curve();
|
||||
|
||||
let min_log_lum = curve.position(0.0).x;
|
||||
let max_log_lum = curve.position(curve.segments().len() as f32).x;
|
||||
let log_lum_range = max_log_lum - min_log_lum;
|
||||
|
||||
let mut lut = [0.0; LUT_SIZE];
|
||||
|
||||
let mut previous = curve.position(0.0);
|
||||
let mut min_compensation = previous.y;
|
||||
let mut max_compensation = previous.y;
|
||||
|
||||
for segment in curve {
|
||||
if segment.position(0.0) != previous {
|
||||
return Err(AutoExposureCompensationCurveError::DiscontinuityFound);
|
||||
}
|
||||
|
||||
for i in 1..Self::SAMPLES_PER_SEGMENT {
|
||||
let current = segment.position(i as f32 / (Self::SAMPLES_PER_SEGMENT - 1) as f32);
|
||||
|
||||
if current.x < previous.x {
|
||||
return Err(AutoExposureCompensationCurveError::NotMonotonic);
|
||||
}
|
||||
|
||||
// Find the range of LUT entries that this line segment covers.
|
||||
let (lut_begin, lut_end) = (
|
||||
((previous.x - min_log_lum) / log_lum_range) * (LUT_SIZE - 1) as f32,
|
||||
((current.x - min_log_lum) / log_lum_range) * (LUT_SIZE - 1) as f32,
|
||||
);
|
||||
let lut_inv_range = 1.0 / (lut_end - lut_begin);
|
||||
|
||||
// Iterate over all LUT entries whose pixel centers fall within the current segment.
|
||||
#[allow(clippy::needless_range_loop)]
|
||||
for i in lut_begin.ceil() as usize..=lut_end.floor() as usize {
|
||||
let t = (i as f32 - lut_begin) * lut_inv_range;
|
||||
lut[i] = previous.y.lerp(current.y, t);
|
||||
min_compensation = min_compensation.min(lut[i]);
|
||||
max_compensation = max_compensation.max(lut[i]);
|
||||
}
|
||||
|
||||
previous = current;
|
||||
}
|
||||
}
|
||||
|
||||
let compensation_range = max_compensation - min_compensation;
|
||||
|
||||
Ok(Self {
|
||||
min_log_lum,
|
||||
max_log_lum,
|
||||
min_compensation,
|
||||
max_compensation,
|
||||
lut: if compensation_range > 0.0 {
|
||||
let scale = 255.0 / compensation_range;
|
||||
lut.map(|f: f32| ((f - min_compensation) * scale) as u8)
|
||||
} else {
|
||||
[0; LUT_SIZE]
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// The GPU-representation of an [`AutoExposureCompensationCurve`].
|
||||
/// Consists of a [`TextureView`] with the curve's data,
|
||||
/// and a [`UniformBuffer`] with the curve's extents.
|
||||
pub struct GpuAutoExposureCompensationCurve {
|
||||
pub(super) texture_view: TextureView,
|
||||
pub(super) extents: UniformBuffer<AutoExposureCompensationCurveUniform>,
|
||||
}
|
||||
|
||||
#[derive(ShaderType, Clone, Copy)]
|
||||
pub(super) struct AutoExposureCompensationCurveUniform {
|
||||
min_log_lum: f32,
|
||||
inv_log_lum_range: f32,
|
||||
min_compensation: f32,
|
||||
compensation_range: f32,
|
||||
}
|
||||
|
||||
impl RenderAsset for GpuAutoExposureCompensationCurve {
|
||||
type SourceAsset = AutoExposureCompensationCurve;
|
||||
type Param = (SRes<RenderDevice>, SRes<RenderQueue>);
|
||||
|
||||
fn asset_usage(_: &Self::SourceAsset) -> RenderAssetUsages {
|
||||
RenderAssetUsages::RENDER_WORLD
|
||||
}
|
||||
|
||||
fn prepare_asset(
|
||||
source: Self::SourceAsset,
|
||||
(render_device, render_queue): &mut SystemParamItem<Self::Param>,
|
||||
) -> Result<Self, bevy_render::render_asset::PrepareAssetError<Self::SourceAsset>> {
|
||||
let texture = render_device.create_texture_with_data(
|
||||
render_queue,
|
||||
&TextureDescriptor {
|
||||
label: None,
|
||||
size: Extent3d {
|
||||
width: LUT_SIZE as u32,
|
||||
height: 1,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: TextureDimension::D1,
|
||||
format: TextureFormat::R8Unorm,
|
||||
usage: TextureUsages::COPY_DST | TextureUsages::TEXTURE_BINDING,
|
||||
view_formats: &[TextureFormat::R8Unorm],
|
||||
},
|
||||
Default::default(),
|
||||
&source.lut,
|
||||
);
|
||||
|
||||
let texture_view = texture.create_view(&Default::default());
|
||||
|
||||
let mut extents = UniformBuffer::from(AutoExposureCompensationCurveUniform {
|
||||
min_log_lum: source.min_log_lum,
|
||||
inv_log_lum_range: 1.0 / (source.max_log_lum - source.min_log_lum),
|
||||
min_compensation: source.min_compensation,
|
||||
compensation_range: source.max_compensation - source.min_compensation,
|
||||
});
|
||||
|
||||
extents.write_buffer(render_device, render_queue);
|
||||
|
||||
Ok(GpuAutoExposureCompensationCurve {
|
||||
texture_view,
|
||||
extents,
|
||||
})
|
||||
}
|
||||
}
|
131
crates/bevy_core_pipeline/src/auto_exposure/mod.rs
Normal file
131
crates/bevy_core_pipeline/src/auto_exposure/mod.rs
Normal file
|
@ -0,0 +1,131 @@
|
|||
use bevy_app::prelude::*;
|
||||
use bevy_asset::{load_internal_asset, AssetApp, Assets, Handle};
|
||||
use bevy_ecs::prelude::*;
|
||||
use bevy_render::extract_component::ExtractComponentPlugin;
|
||||
use bevy_render::render_asset::RenderAssetPlugin;
|
||||
use bevy_render::render_resource::Shader;
|
||||
use bevy_render::ExtractSchedule;
|
||||
use bevy_render::{
|
||||
render_graph::RenderGraphApp,
|
||||
render_resource::{
|
||||
Buffer, BufferDescriptor, BufferUsages, PipelineCache, SpecializedComputePipelines,
|
||||
},
|
||||
renderer::RenderDevice,
|
||||
Render, RenderApp, RenderSet,
|
||||
};
|
||||
|
||||
mod buffers;
|
||||
mod compensation_curve;
|
||||
mod node;
|
||||
mod pipeline;
|
||||
mod settings;
|
||||
|
||||
use buffers::{extract_buffers, prepare_buffers, AutoExposureBuffers};
|
||||
pub use compensation_curve::{AutoExposureCompensationCurve, AutoExposureCompensationCurveError};
|
||||
use node::AutoExposureNode;
|
||||
use pipeline::{
|
||||
AutoExposurePass, AutoExposurePipeline, ViewAutoExposurePipeline, METERING_SHADER_HANDLE,
|
||||
};
|
||||
pub use settings::AutoExposureSettings;
|
||||
|
||||
use crate::auto_exposure::compensation_curve::GpuAutoExposureCompensationCurve;
|
||||
use crate::core_3d::graph::{Core3d, Node3d};
|
||||
|
||||
/// Plugin for the auto exposure feature.
|
||||
///
|
||||
/// See [`AutoExposureSettings`] for more details.
|
||||
pub struct AutoExposurePlugin;
|
||||
|
||||
#[derive(Resource)]
|
||||
struct AutoExposureResources {
|
||||
histogram: Buffer,
|
||||
}
|
||||
|
||||
impl Plugin for AutoExposurePlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
load_internal_asset!(
|
||||
app,
|
||||
METERING_SHADER_HANDLE,
|
||||
"auto_exposure.wgsl",
|
||||
Shader::from_wgsl
|
||||
);
|
||||
|
||||
app.add_plugins(RenderAssetPlugin::<GpuAutoExposureCompensationCurve>::default())
|
||||
.register_type::<AutoExposureCompensationCurve>()
|
||||
.init_asset::<AutoExposureCompensationCurve>()
|
||||
.register_asset_reflect::<AutoExposureCompensationCurve>();
|
||||
app.world_mut()
|
||||
.resource_mut::<Assets<AutoExposureCompensationCurve>>()
|
||||
.insert(&Handle::default(), AutoExposureCompensationCurve::default());
|
||||
|
||||
app.register_type::<AutoExposureSettings>();
|
||||
app.add_plugins(ExtractComponentPlugin::<AutoExposureSettings>::default());
|
||||
|
||||
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
|
||||
return;
|
||||
};
|
||||
|
||||
render_app
|
||||
.init_resource::<SpecializedComputePipelines<AutoExposurePipeline>>()
|
||||
.init_resource::<AutoExposureBuffers>()
|
||||
.add_systems(ExtractSchedule, extract_buffers)
|
||||
.add_systems(
|
||||
Render,
|
||||
(
|
||||
prepare_buffers.in_set(RenderSet::Prepare),
|
||||
queue_view_auto_exposure_pipelines.in_set(RenderSet::Queue),
|
||||
),
|
||||
)
|
||||
.add_render_graph_node::<AutoExposureNode>(Core3d, node::AutoExposure)
|
||||
.add_render_graph_edges(
|
||||
Core3d,
|
||||
(Node3d::EndMainPass, node::AutoExposure, Node3d::Tonemapping),
|
||||
);
|
||||
}
|
||||
|
||||
fn finish(&self, app: &mut App) {
|
||||
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
|
||||
return;
|
||||
};
|
||||
|
||||
render_app.init_resource::<AutoExposurePipeline>();
|
||||
render_app.init_resource::<AutoExposureResources>();
|
||||
}
|
||||
}
|
||||
|
||||
impl FromWorld for AutoExposureResources {
|
||||
fn from_world(world: &mut World) -> Self {
|
||||
Self {
|
||||
histogram: world
|
||||
.resource::<RenderDevice>()
|
||||
.create_buffer(&BufferDescriptor {
|
||||
label: Some("histogram buffer"),
|
||||
size: pipeline::HISTOGRAM_BIN_COUNT * 4,
|
||||
usage: BufferUsages::STORAGE,
|
||||
mapped_at_creation: false,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn queue_view_auto_exposure_pipelines(
|
||||
mut commands: Commands,
|
||||
pipeline_cache: ResMut<PipelineCache>,
|
||||
mut compute_pipelines: ResMut<SpecializedComputePipelines<AutoExposurePipeline>>,
|
||||
pipeline: Res<AutoExposurePipeline>,
|
||||
view_targets: Query<(Entity, &AutoExposureSettings)>,
|
||||
) {
|
||||
for (entity, settings) in view_targets.iter() {
|
||||
let histogram_pipeline =
|
||||
compute_pipelines.specialize(&pipeline_cache, &pipeline, AutoExposurePass::Histogram);
|
||||
let average_pipeline =
|
||||
compute_pipelines.specialize(&pipeline_cache, &pipeline, AutoExposurePass::Average);
|
||||
|
||||
commands.entity(entity).insert(ViewAutoExposurePipeline {
|
||||
histogram_pipeline,
|
||||
mean_luminance_pipeline: average_pipeline,
|
||||
compensation_curve: settings.compensation_curve.clone(),
|
||||
metering_mask: settings.metering_mask.clone(),
|
||||
});
|
||||
}
|
||||
}
|
141
crates/bevy_core_pipeline/src/auto_exposure/node.rs
Normal file
141
crates/bevy_core_pipeline/src/auto_exposure/node.rs
Normal file
|
@ -0,0 +1,141 @@
|
|||
use super::{
|
||||
buffers::AutoExposureBuffers,
|
||||
compensation_curve::GpuAutoExposureCompensationCurve,
|
||||
pipeline::{AutoExposurePipeline, ViewAutoExposurePipeline},
|
||||
AutoExposureResources,
|
||||
};
|
||||
use bevy_ecs::{
|
||||
query::QueryState,
|
||||
system::lifetimeless::Read,
|
||||
world::{FromWorld, World},
|
||||
};
|
||||
use bevy_render::{
|
||||
globals::GlobalsBuffer,
|
||||
render_asset::RenderAssets,
|
||||
render_graph::*,
|
||||
render_resource::*,
|
||||
renderer::RenderContext,
|
||||
texture::{FallbackImage, GpuImage},
|
||||
view::{ExtractedView, ViewTarget, ViewUniform, ViewUniformOffset, ViewUniforms},
|
||||
};
|
||||
|
||||
#[derive(RenderLabel, Debug, Clone, Hash, PartialEq, Eq)]
|
||||
pub struct AutoExposure;
|
||||
|
||||
pub struct AutoExposureNode {
|
||||
query: QueryState<(
|
||||
Read<ViewUniformOffset>,
|
||||
Read<ViewTarget>,
|
||||
Read<ViewAutoExposurePipeline>,
|
||||
Read<ExtractedView>,
|
||||
)>,
|
||||
}
|
||||
|
||||
impl FromWorld for AutoExposureNode {
|
||||
fn from_world(world: &mut World) -> Self {
|
||||
Self {
|
||||
query: QueryState::new(world),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Node for AutoExposureNode {
|
||||
fn update(&mut self, world: &mut World) {
|
||||
self.query.update_archetypes(world);
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
graph: &mut RenderGraphContext,
|
||||
render_context: &mut RenderContext,
|
||||
world: &World,
|
||||
) -> Result<(), NodeRunError> {
|
||||
let view_entity = graph.view_entity();
|
||||
let pipeline_cache = world.resource::<PipelineCache>();
|
||||
let pipeline = world.resource::<AutoExposurePipeline>();
|
||||
let resources = world.resource::<AutoExposureResources>();
|
||||
|
||||
let view_uniforms_resource = world.resource::<ViewUniforms>();
|
||||
let view_uniforms = &view_uniforms_resource.uniforms;
|
||||
let view_uniforms_buffer = view_uniforms.buffer().unwrap();
|
||||
|
||||
let globals_buffer = world.resource::<GlobalsBuffer>();
|
||||
|
||||
let auto_exposure_buffers = world.resource::<AutoExposureBuffers>();
|
||||
|
||||
let (
|
||||
Ok((view_uniform_offset, view_target, auto_exposure, view)),
|
||||
Some(auto_exposure_buffers),
|
||||
) = (
|
||||
self.query.get_manual(world, view_entity),
|
||||
auto_exposure_buffers.buffers.get(&view_entity),
|
||||
)
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let (Some(histogram_pipeline), Some(average_pipeline)) = (
|
||||
pipeline_cache.get_compute_pipeline(auto_exposure.histogram_pipeline),
|
||||
pipeline_cache.get_compute_pipeline(auto_exposure.mean_luminance_pipeline),
|
||||
) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let source = view_target.main_texture_view();
|
||||
|
||||
let fallback = world.resource::<FallbackImage>();
|
||||
let mask = world
|
||||
.resource::<RenderAssets<GpuImage>>()
|
||||
.get(&auto_exposure.metering_mask);
|
||||
let mask = mask
|
||||
.map(|i| &i.texture_view)
|
||||
.unwrap_or(&fallback.d2.texture_view);
|
||||
|
||||
let Some(compensation_curve) = world
|
||||
.resource::<RenderAssets<GpuAutoExposureCompensationCurve>>()
|
||||
.get(&auto_exposure.compensation_curve)
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let compute_bind_group = render_context.render_device().create_bind_group(
|
||||
None,
|
||||
&pipeline.histogram_layout,
|
||||
&BindGroupEntries::sequential((
|
||||
&globals_buffer.buffer,
|
||||
&auto_exposure_buffers.settings,
|
||||
source,
|
||||
mask,
|
||||
&compensation_curve.texture_view,
|
||||
&compensation_curve.extents,
|
||||
resources.histogram.as_entire_buffer_binding(),
|
||||
&auto_exposure_buffers.state,
|
||||
BufferBinding {
|
||||
buffer: view_uniforms_buffer,
|
||||
size: Some(ViewUniform::min_size()),
|
||||
offset: 0,
|
||||
},
|
||||
)),
|
||||
);
|
||||
|
||||
let mut compute_pass =
|
||||
render_context
|
||||
.command_encoder()
|
||||
.begin_compute_pass(&ComputePassDescriptor {
|
||||
label: Some("auto_exposure_pass"),
|
||||
timestamp_writes: None,
|
||||
});
|
||||
|
||||
compute_pass.set_bind_group(0, &compute_bind_group, &[view_uniform_offset.offset]);
|
||||
compute_pass.set_pipeline(histogram_pipeline);
|
||||
compute_pass.dispatch_workgroups(
|
||||
view.viewport.z.div_ceil(16),
|
||||
view.viewport.w.div_ceil(16),
|
||||
1,
|
||||
);
|
||||
compute_pass.set_pipeline(average_pipeline);
|
||||
compute_pass.dispatch_workgroups(1, 1, 1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
94
crates/bevy_core_pipeline/src/auto_exposure/pipeline.rs
Normal file
94
crates/bevy_core_pipeline/src/auto_exposure/pipeline.rs
Normal file
|
@ -0,0 +1,94 @@
|
|||
use super::compensation_curve::{
|
||||
AutoExposureCompensationCurve, AutoExposureCompensationCurveUniform,
|
||||
};
|
||||
use bevy_asset::prelude::*;
|
||||
use bevy_ecs::prelude::*;
|
||||
use bevy_render::{
|
||||
globals::GlobalsUniform,
|
||||
render_resource::{binding_types::*, *},
|
||||
renderer::RenderDevice,
|
||||
texture::Image,
|
||||
view::ViewUniform,
|
||||
};
|
||||
use std::num::NonZeroU64;
|
||||
|
||||
#[derive(Resource)]
|
||||
pub struct AutoExposurePipeline {
|
||||
pub histogram_layout: BindGroupLayout,
|
||||
pub histogram_shader: Handle<Shader>,
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct ViewAutoExposurePipeline {
|
||||
pub histogram_pipeline: CachedComputePipelineId,
|
||||
pub mean_luminance_pipeline: CachedComputePipelineId,
|
||||
pub compensation_curve: Handle<AutoExposureCompensationCurve>,
|
||||
pub metering_mask: Handle<Image>,
|
||||
}
|
||||
|
||||
#[derive(ShaderType, Clone, Copy)]
|
||||
pub struct AutoExposureSettingsUniform {
|
||||
pub(super) min_log_lum: f32,
|
||||
pub(super) inv_log_lum_range: f32,
|
||||
pub(super) log_lum_range: f32,
|
||||
pub(super) low_percent: f32,
|
||||
pub(super) high_percent: f32,
|
||||
pub(super) speed_up: f32,
|
||||
pub(super) speed_down: f32,
|
||||
pub(super) exponential_transition_distance: f32,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Hash, Clone)]
|
||||
pub enum AutoExposurePass {
|
||||
Histogram,
|
||||
Average,
|
||||
}
|
||||
|
||||
pub const METERING_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(12987620402995522466);
|
||||
|
||||
pub const HISTOGRAM_BIN_COUNT: u64 = 64;
|
||||
|
||||
impl FromWorld for AutoExposurePipeline {
|
||||
fn from_world(world: &mut World) -> Self {
|
||||
let render_device = world.resource::<RenderDevice>();
|
||||
|
||||
Self {
|
||||
histogram_layout: render_device.create_bind_group_layout(
|
||||
"compute histogram bind group",
|
||||
&BindGroupLayoutEntries::sequential(
|
||||
ShaderStages::COMPUTE,
|
||||
(
|
||||
uniform_buffer::<GlobalsUniform>(false),
|
||||
uniform_buffer::<AutoExposureSettingsUniform>(false),
|
||||
texture_2d(TextureSampleType::Float { filterable: false }),
|
||||
texture_2d(TextureSampleType::Float { filterable: false }),
|
||||
texture_1d(TextureSampleType::Float { filterable: false }),
|
||||
uniform_buffer::<AutoExposureCompensationCurveUniform>(false),
|
||||
storage_buffer_sized(false, NonZeroU64::new(HISTOGRAM_BIN_COUNT * 4)),
|
||||
storage_buffer_sized(false, NonZeroU64::new(4)),
|
||||
storage_buffer::<ViewUniform>(true),
|
||||
),
|
||||
),
|
||||
),
|
||||
histogram_shader: METERING_SHADER_HANDLE.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SpecializedComputePipeline for AutoExposurePipeline {
|
||||
type Key = AutoExposurePass;
|
||||
|
||||
fn specialize(&self, pass: AutoExposurePass) -> ComputePipelineDescriptor {
|
||||
ComputePipelineDescriptor {
|
||||
label: Some("luminance compute pipeline".into()),
|
||||
layout: vec![self.histogram_layout.clone()],
|
||||
shader: self.histogram_shader.clone(),
|
||||
shader_defs: vec![],
|
||||
entry_point: match pass {
|
||||
AutoExposurePass::Histogram => "compute_histogram".into(),
|
||||
AutoExposurePass::Average => "compute_average".into(),
|
||||
},
|
||||
push_constant_ranges: vec![],
|
||||
}
|
||||
}
|
||||
}
|
102
crates/bevy_core_pipeline/src/auto_exposure/settings.rs
Normal file
102
crates/bevy_core_pipeline/src/auto_exposure/settings.rs
Normal file
|
@ -0,0 +1,102 @@
|
|||
use std::ops::RangeInclusive;
|
||||
|
||||
use super::compensation_curve::AutoExposureCompensationCurve;
|
||||
use bevy_asset::Handle;
|
||||
use bevy_ecs::{prelude::Component, reflect::ReflectComponent};
|
||||
use bevy_reflect::Reflect;
|
||||
use bevy_render::{extract_component::ExtractComponent, texture::Image};
|
||||
use bevy_utils::default;
|
||||
|
||||
/// Component that enables auto exposure for an HDR-enabled 2d or 3d camera.
|
||||
///
|
||||
/// Auto exposure adjusts the exposure of the camera automatically to
|
||||
/// simulate the human eye's ability to adapt to different lighting conditions.
|
||||
///
|
||||
/// Bevy's implementation builds a 64 bin histogram of the scene's luminance,
|
||||
/// and then adjusts the exposure so that the average brightness of the final
|
||||
/// render will be middle gray. Because it's using a histogram, some details can
|
||||
/// be selectively ignored or emphasized. Outliers like shadows and specular
|
||||
/// highlights can be ignored, and certain areas can be given more (or less)
|
||||
/// weight based on a mask.
|
||||
///
|
||||
/// # Usage Notes
|
||||
///
|
||||
/// **Auto Exposure requires compute shaders and is not compatible with WebGL2.**
|
||||
///
|
||||
#[derive(Component, Clone, Reflect, ExtractComponent)]
|
||||
#[reflect(Component)]
|
||||
pub struct AutoExposureSettings {
|
||||
/// The range of exposure values for the histogram.
|
||||
///
|
||||
/// Pixel values below this range will be ignored, and pixel values above this range will be
|
||||
/// clamped in the sense that they will count towards the highest bin in the histogram.
|
||||
/// The default value is `-8.0..=8.0`.
|
||||
pub range: RangeInclusive<f32>,
|
||||
|
||||
/// The portion of the histogram to consider when metering.
|
||||
///
|
||||
/// By default, the darkest 10% and the brightest 10% of samples are ignored,
|
||||
/// so the default value is `0.10..=0.90`.
|
||||
pub filter: RangeInclusive<f32>,
|
||||
|
||||
/// The speed at which the exposure adapts from dark to bright scenes, in F-stops per second.
|
||||
pub speed_brighten: f32,
|
||||
|
||||
/// The speed at which the exposure adapts from bright to dark scenes, in F-stops per second.
|
||||
pub speed_darken: f32,
|
||||
|
||||
/// The distance in F-stops from the target exposure from where to transition from animating
|
||||
/// in linear fashion to animating exponentially. This helps against jittering when the
|
||||
/// target exposure keeps on changing slightly from frame to frame, while still maintaining
|
||||
/// a relatively slow animation for big changes in scene brightness.
|
||||
///
|
||||
/// ```text
|
||||
/// ev
|
||||
/// ➔●┐
|
||||
/// | ⬈ ├ exponential section
|
||||
/// │ ⬈ ┘
|
||||
/// │ ⬈ ┐
|
||||
/// │ ⬈ ├ linear section
|
||||
/// │⬈ ┘
|
||||
/// ●───────────────────────── time
|
||||
/// ```
|
||||
///
|
||||
/// The default value is 1.5.
|
||||
pub exponential_transition_distance: f32,
|
||||
|
||||
/// The mask to apply when metering. The mask will cover the entire screen, where:
|
||||
/// * `(0.0, 0.0)` is the top-left corner,
|
||||
/// * `(1.0, 1.0)` is the bottom-right corner.
|
||||
/// Only the red channel of the texture is used.
|
||||
/// The sample at the current screen position will be used to weight the contribution
|
||||
/// of each pixel to the histogram:
|
||||
/// * 0.0 means the pixel will not contribute to the histogram,
|
||||
/// * 1.0 means the pixel will contribute fully to the histogram.
|
||||
///
|
||||
/// The default value is a white image, so all pixels contribute equally.
|
||||
///
|
||||
/// # Usage Notes
|
||||
///
|
||||
/// The mask is quantized to 16 discrete levels because of limitations in the compute shader
|
||||
/// implementation.
|
||||
pub metering_mask: Handle<Image>,
|
||||
|
||||
/// Exposure compensation curve to apply after metering.
|
||||
/// The default value is a flat line at 0.0.
|
||||
/// For more information, see [`AutoExposureCompensationCurve`].
|
||||
pub compensation_curve: Handle<AutoExposureCompensationCurve>,
|
||||
}
|
||||
|
||||
impl Default for AutoExposureSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
range: -8.0..=8.0,
|
||||
filter: 0.10..=0.90,
|
||||
speed_brighten: 3.0,
|
||||
speed_darken: 1.0,
|
||||
exponential_transition_distance: 1.5,
|
||||
metering_mask: default(),
|
||||
compensation_curve: default(),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -28,6 +28,7 @@ pub mod graph {
|
|||
Taa,
|
||||
MotionBlur,
|
||||
Bloom,
|
||||
AutoExposure,
|
||||
Tonemapping,
|
||||
Fxaa,
|
||||
Upscaling,
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
html_favicon_url = "https://bevyengine.org/assets/icon.png"
|
||||
)]
|
||||
|
||||
pub mod auto_exposure;
|
||||
pub mod blit;
|
||||
pub mod bloom;
|
||||
pub mod contrast_adaptive_sharpening;
|
||||
|
|
|
@ -30,7 +30,7 @@ fn sample_current_lut(p: vec3<f32>) -> vec3<f32> {
|
|||
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
|
||||
#else
|
||||
return vec3(1.0, 0.0, 1.0);
|
||||
#endif
|
||||
}
|
||||
|
@ -42,8 +42,8 @@ fn sample_current_lut(p: vec3<f32>) -> vec3<f32> {
|
|||
|
||||
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.2126, 0.7152, 0.0722,
|
||||
-0.1146, -0.3854, 0.5,
|
||||
0.5, -0.4542, -0.0458
|
||||
);
|
||||
return col * m;
|
||||
|
@ -51,8 +51,8 @@ fn rgb_to_ycbcr(col: vec3<f32>) -> vec3<f32> {
|
|||
|
||||
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, 0.0, 1.5748,
|
||||
1.0, -0.1873, -0.4681,
|
||||
1.0, 1.8556, 0.0
|
||||
);
|
||||
return max(vec3(0.0), col * m);
|
||||
|
@ -122,14 +122,14 @@ fn RRTAndODTFit(v: vec3<f32>) -> vec3<f32> {
|
|||
return a / b;
|
||||
}
|
||||
|
||||
fn ACESFitted(color: vec3<f32>) -> vec3<f32> {
|
||||
fn ACESFitted(color: vec3<f32>) -> vec3<f32> {
|
||||
var fitted_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)
|
||||
vec3(0.02840, 0.13383, 0.83777)
|
||||
);
|
||||
|
||||
// ODT_SAT => XYZ => D60_2_D65 => sRGB
|
||||
|
@ -224,7 +224,7 @@ fn applyAgXLog(Image: vec3<f32>) -> vec3<f32> {
|
|||
prepared_image = vec3(r, g, b);
|
||||
|
||||
prepared_image = convertOpenDomainToNormalizedLog2_(prepared_image, -10.0, 6.5);
|
||||
|
||||
|
||||
prepared_image = clamp(prepared_image, vec3(0.0), vec3(1.0));
|
||||
return prepared_image;
|
||||
}
|
||||
|
@ -368,6 +368,10 @@ fn tone_mapping(in: vec4<f32>, in_color_grading: ColorGrading) -> vec4<f32> {
|
|||
// applies individually to shadows, midtones, and highlights.
|
||||
#ifdef SECTIONAL_COLOR_GRADING
|
||||
color = sectional_color_grading(color, &color_grading);
|
||||
#else
|
||||
// If we're not doing sectional color grading, the exposure might still need
|
||||
// to be applied, for example when using auto exposure.
|
||||
color = color * powsafe(vec3(2.0), color_grading.exposure);
|
||||
#endif
|
||||
|
||||
// tone_mapping
|
||||
|
@ -385,14 +389,14 @@ fn tone_mapping(in: vec4<f32>, in_color_grading: ColorGrading) -> vec4<f32> {
|
|||
#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);
|
||||
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, color_grading.post_saturation);
|
||||
|
||||
|
||||
return vec4(color, in.a);
|
||||
}
|
||||
|
||||
|
|
|
@ -408,6 +408,15 @@ pub mod binding_types {
|
|||
.into_bind_group_layout_entry_builder()
|
||||
}
|
||||
|
||||
pub fn texture_1d(sample_type: TextureSampleType) -> BindGroupLayoutEntryBuilder {
|
||||
BindingType::Texture {
|
||||
sample_type,
|
||||
view_dimension: TextureViewDimension::D1,
|
||||
multisampled: false,
|
||||
}
|
||||
.into_bind_group_layout_entry_builder()
|
||||
}
|
||||
|
||||
pub fn texture_2d(sample_type: TextureSampleType) -> BindGroupLayoutEntryBuilder {
|
||||
BindingType::Texture {
|
||||
sample_type,
|
||||
|
|
|
@ -8,6 +8,8 @@ use encase::{
|
|||
};
|
||||
use wgpu::{util::BufferInitDescriptor, BindingResource, BufferBinding, BufferUsages};
|
||||
|
||||
use super::IntoBinding;
|
||||
|
||||
/// Stores data to be transferred to the GPU and made accessible to shaders as a storage buffer.
|
||||
///
|
||||
/// Storage buffers can be made available to shaders in some combination of read/write mode, and can store large amounts of data.
|
||||
|
@ -138,6 +140,16 @@ impl<T: ShaderType + WriteInto> StorageBuffer<T> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, T: ShaderType + WriteInto> IntoBinding<'a> for &'a StorageBuffer<T> {
|
||||
#[inline]
|
||||
fn into_binding(self) -> BindingResource<'a> {
|
||||
self.buffer()
|
||||
.expect("Failed to get buffer")
|
||||
.as_entire_buffer_binding()
|
||||
.into_binding()
|
||||
}
|
||||
}
|
||||
|
||||
/// Stores data to be transferred to the GPU and made accessible to shaders as a dynamic storage buffer.
|
||||
///
|
||||
/// Dynamic storage buffers can be made available to shaders in some combination of read/write mode, and can store large amounts
|
||||
|
@ -256,3 +268,10 @@ impl<T: ShaderType + WriteInto> DynamicStorageBuffer<T> {
|
|||
self.scratch.set_offset(0);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: ShaderType + WriteInto> IntoBinding<'a> for &'a DynamicStorageBuffer<T> {
|
||||
#[inline]
|
||||
fn into_binding(self) -> BindingResource<'a> {
|
||||
self.binding().unwrap()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@ use std::{
|
|||
},
|
||||
};
|
||||
use wgpu::{
|
||||
Extent3d, RenderPassColorAttachment, RenderPassDepthStencilAttachment, StoreOp,
|
||||
BufferUsages, Extent3d, RenderPassColorAttachment, RenderPassDepthStencilAttachment, StoreOp,
|
||||
TextureDescriptor, TextureDimension, TextureFormat, TextureUsages,
|
||||
};
|
||||
|
||||
|
@ -114,7 +114,7 @@ impl Plugin for ViewPlugin {
|
|||
));
|
||||
|
||||
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
|
||||
render_app.init_resource::<ViewUniforms>().add_systems(
|
||||
render_app.add_systems(
|
||||
Render,
|
||||
(
|
||||
prepare_view_targets
|
||||
|
@ -127,6 +127,12 @@ impl Plugin for ViewPlugin {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn finish(&self, app: &mut App) {
|
||||
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
|
||||
render_app.init_resource::<ViewUniforms>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration resource for [Multi-Sample Anti-Aliasing](https://en.wikipedia.org/wiki/Multisample_anti-aliasing).
|
||||
|
@ -415,11 +421,24 @@ pub struct ViewUniform {
|
|||
render_layers: u32,
|
||||
}
|
||||
|
||||
#[derive(Resource, Default)]
|
||||
#[derive(Resource)]
|
||||
pub struct ViewUniforms {
|
||||
pub uniforms: DynamicUniformBuffer<ViewUniform>,
|
||||
}
|
||||
|
||||
impl FromWorld for ViewUniforms {
|
||||
fn from_world(world: &mut World) -> Self {
|
||||
let mut uniforms = DynamicUniformBuffer::default();
|
||||
|
||||
let render_device = world.resource::<RenderDevice>();
|
||||
if render_device.limits().max_storage_buffers_per_shader_stage > 0 {
|
||||
uniforms.add_usages(BufferUsages::STORAGE);
|
||||
}
|
||||
|
||||
Self { uniforms }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct ViewUniformOffset {
|
||||
pub offset: u32,
|
||||
|
|
231
examples/3d/auto_exposure.rs
Normal file
231
examples/3d/auto_exposure.rs
Normal file
|
@ -0,0 +1,231 @@
|
|||
//! This example showcases auto exposure,
|
||||
//! which automatically (but not instantly) adjusts the brightness of the scene in a way that mimics the function of the human eye.
|
||||
//! Auto exposure requires compute shader capabilities, so it's not available on WebGL.
|
||||
//!
|
||||
//! ## Controls
|
||||
//!
|
||||
//! | Key Binding | Action |
|
||||
//! |:-------------------|:---------------------------------------|
|
||||
//! | `Left` / `Right` | Rotate Camera |
|
||||
//! | `C` | Toggle Compensation Curve |
|
||||
//! | `M` | Toggle Metering Mask |
|
||||
//! | `V` | Visualize Metering Mask |
|
||||
|
||||
use bevy::{
|
||||
core_pipeline::{
|
||||
auto_exposure::{AutoExposureCompensationCurve, AutoExposurePlugin, AutoExposureSettings},
|
||||
Skybox,
|
||||
},
|
||||
math::{cubic_splines::LinearSpline, primitives::Plane3d, vec2},
|
||||
prelude::*,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
App::new()
|
||||
.add_plugins(DefaultPlugins)
|
||||
.add_plugins(AutoExposurePlugin)
|
||||
.add_systems(Startup, setup)
|
||||
.add_systems(Update, example_control_system)
|
||||
.run();
|
||||
}
|
||||
|
||||
fn setup(
|
||||
mut commands: Commands,
|
||||
mut meshes: ResMut<Assets<Mesh>>,
|
||||
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||
mut compensation_curves: ResMut<Assets<AutoExposureCompensationCurve>>,
|
||||
asset_server: Res<AssetServer>,
|
||||
) {
|
||||
let metering_mask = asset_server.load("textures/basic_metering_mask.png");
|
||||
|
||||
commands.spawn((
|
||||
Camera3dBundle {
|
||||
camera: Camera {
|
||||
hdr: true,
|
||||
..default()
|
||||
},
|
||||
transform: Transform::from_xyz(1.0, 0.0, 0.0).looking_at(Vec3::ZERO, Vec3::Y),
|
||||
..default()
|
||||
},
|
||||
AutoExposureSettings {
|
||||
metering_mask: metering_mask.clone(),
|
||||
..default()
|
||||
},
|
||||
Skybox {
|
||||
image: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"),
|
||||
brightness: bevy::pbr::light_consts::lux::DIRECT_SUNLIGHT,
|
||||
},
|
||||
));
|
||||
|
||||
commands.insert_resource(ExampleResources {
|
||||
basic_compensation_curve: compensation_curves.add(
|
||||
AutoExposureCompensationCurve::from_curve(LinearSpline::new([
|
||||
vec2(-4.0, -2.0),
|
||||
vec2(0.0, 0.0),
|
||||
vec2(2.0, 0.0),
|
||||
vec2(4.0, 2.0),
|
||||
]))
|
||||
.unwrap(),
|
||||
),
|
||||
basic_metering_mask: metering_mask.clone(),
|
||||
});
|
||||
|
||||
let plane = meshes.add(Mesh::from(
|
||||
Plane3d {
|
||||
normal: -Dir3::Z,
|
||||
half_size: Vec2::new(2.0, 0.5),
|
||||
}
|
||||
.mesh(),
|
||||
));
|
||||
|
||||
// Build a dimly lit box around the camera, with a slot to see the bright skybox.
|
||||
for level in -1..=1 {
|
||||
for side in [-Vec3::X, Vec3::X, -Vec3::Z, Vec3::Z] {
|
||||
if level == 0 && Vec3::Z == side {
|
||||
continue;
|
||||
}
|
||||
|
||||
let height = Vec3::Y * level as f32;
|
||||
|
||||
commands.spawn(PbrBundle {
|
||||
mesh: plane.clone(),
|
||||
material: materials.add(StandardMaterial {
|
||||
base_color: Color::srgb(
|
||||
0.5 + side.x * 0.5,
|
||||
0.75 - level as f32 * 0.25,
|
||||
0.5 + side.z * 0.5,
|
||||
),
|
||||
..default()
|
||||
}),
|
||||
transform: Transform::from_translation(side * 2.0 + height)
|
||||
.looking_at(height, Vec3::Y),
|
||||
..default()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
commands.insert_resource(AmbientLight {
|
||||
color: Color::WHITE,
|
||||
brightness: 0.0,
|
||||
});
|
||||
|
||||
commands.spawn(PointLightBundle {
|
||||
point_light: PointLight {
|
||||
intensity: 5000.0,
|
||||
..default()
|
||||
},
|
||||
transform: Transform::from_xyz(0.0, 0.0, 0.0),
|
||||
..default()
|
||||
});
|
||||
|
||||
commands.spawn(ImageBundle {
|
||||
image: UiImage {
|
||||
texture: metering_mask,
|
||||
..default()
|
||||
},
|
||||
style: Style {
|
||||
width: Val::Percent(100.0),
|
||||
height: Val::Percent(100.0),
|
||||
..default()
|
||||
},
|
||||
..default()
|
||||
});
|
||||
|
||||
let text_style = TextStyle {
|
||||
font: asset_server.load("fonts/FiraMono-Medium.ttf"),
|
||||
font_size: 18.0,
|
||||
..default()
|
||||
};
|
||||
|
||||
commands.spawn(
|
||||
TextBundle::from_section(
|
||||
"Left / Right — Rotate Camera\nC — Toggle Compensation Curve\nM — Toggle Metering Mask\nV — Visualize Metering Mask",
|
||||
text_style.clone(),
|
||||
)
|
||||
.with_style(Style {
|
||||
position_type: PositionType::Absolute,
|
||||
top: Val::Px(10.0),
|
||||
left: Val::Px(10.0),
|
||||
..default()
|
||||
}),
|
||||
);
|
||||
|
||||
commands.spawn((
|
||||
TextBundle::from_section("", text_style).with_style(Style {
|
||||
position_type: PositionType::Absolute,
|
||||
top: Val::Px(10.0),
|
||||
right: Val::Px(10.0),
|
||||
..default()
|
||||
}),
|
||||
ExampleDisplay,
|
||||
));
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
struct ExampleDisplay;
|
||||
|
||||
#[derive(Resource)]
|
||||
struct ExampleResources {
|
||||
basic_compensation_curve: Handle<AutoExposureCompensationCurve>,
|
||||
basic_metering_mask: Handle<Image>,
|
||||
}
|
||||
|
||||
fn example_control_system(
|
||||
mut camera: Query<(&mut Transform, &mut AutoExposureSettings), With<Camera3d>>,
|
||||
mut display: Query<&mut Text, With<ExampleDisplay>>,
|
||||
mut mask_image: Query<&mut Style, With<UiImage>>,
|
||||
time: Res<Time>,
|
||||
input: Res<ButtonInput<KeyCode>>,
|
||||
resources: Res<ExampleResources>,
|
||||
) {
|
||||
let (mut camera_transform, mut auto_exposure) = camera.single_mut();
|
||||
|
||||
let rotation = if input.pressed(KeyCode::ArrowLeft) {
|
||||
time.delta_seconds()
|
||||
} else if input.pressed(KeyCode::ArrowRight) {
|
||||
-time.delta_seconds()
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
camera_transform.rotate_around(Vec3::ZERO, Quat::from_rotation_y(rotation));
|
||||
|
||||
if input.just_pressed(KeyCode::KeyC) {
|
||||
auto_exposure.compensation_curve =
|
||||
if auto_exposure.compensation_curve == resources.basic_compensation_curve {
|
||||
Handle::default()
|
||||
} else {
|
||||
resources.basic_compensation_curve.clone()
|
||||
};
|
||||
}
|
||||
|
||||
if input.just_pressed(KeyCode::KeyM) {
|
||||
auto_exposure.metering_mask =
|
||||
if auto_exposure.metering_mask == resources.basic_metering_mask {
|
||||
Handle::default()
|
||||
} else {
|
||||
resources.basic_metering_mask.clone()
|
||||
};
|
||||
}
|
||||
|
||||
mask_image.single_mut().display = if input.pressed(KeyCode::KeyV) {
|
||||
Display::Flex
|
||||
} else {
|
||||
Display::None
|
||||
};
|
||||
|
||||
let mut display = display.single_mut();
|
||||
display.sections[0].value = format!(
|
||||
"Compensation Curve: {}\nMetering Mask: {}",
|
||||
if auto_exposure.compensation_curve == resources.basic_compensation_curve {
|
||||
"Enabled"
|
||||
} else {
|
||||
"Disabled"
|
||||
},
|
||||
if auto_exposure.metering_mask == resources.basic_metering_mask {
|
||||
"Enabled"
|
||||
} else {
|
||||
"Disabled"
|
||||
},
|
||||
);
|
||||
}
|
|
@ -128,6 +128,7 @@ Example | Description
|
|||
[Animated Material](../examples/3d/animated_material.rs) | Shows how to animate material properties
|
||||
[Anti-aliasing](../examples/3d/anti_aliasing.rs) | Compares different anti-aliasing methods
|
||||
[Atmospheric Fog](../examples/3d/atmospheric_fog.rs) | A scene showcasing the atmospheric fog effect
|
||||
[Auto Exposure](../examples/3d/auto_exposure.rs) | A scene showcasing auto exposure
|
||||
[Blend Modes](../examples/3d/blend_modes.rs) | Showcases different blend modes
|
||||
[Color grading](../examples/3d/color_grading.rs) | Demonstrates color grading
|
||||
[Deferred Rendering](../examples/3d/deferred_rendering.rs) | Renders meshes with both forward and deferred pipelines
|
||||
|
|
Loading…
Reference in a new issue