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
11
Cargo.toml
11
Cargo.toml
@ -677,6 +677,17 @@ description = "A scene showcasing the distance fog effect"
|
|||||||
category = "3D Rendering"
|
category = "3D Rendering"
|
||||||
wasm = true
|
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]]
|
[[example]]
|
||||||
name = "blend_modes"
|
name = "blend_modes"
|
||||||
path = "examples/3d/blend_modes.rs"
|
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"
|
bitflags = "2.3"
|
||||||
radsort = "0.1"
|
radsort = "0.1"
|
||||||
nonmax = "0.5"
|
nonmax = "0.5"
|
||||||
|
thiserror = "1.0"
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
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,
|
Taa,
|
||||||
MotionBlur,
|
MotionBlur,
|
||||||
Bloom,
|
Bloom,
|
||||||
|
AutoExposure,
|
||||||
Tonemapping,
|
Tonemapping,
|
||||||
Fxaa,
|
Fxaa,
|
||||||
Upscaling,
|
Upscaling,
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
html_favicon_url = "https://bevyengine.org/assets/icon.png"
|
html_favicon_url = "https://bevyengine.org/assets/icon.png"
|
||||||
)]
|
)]
|
||||||
|
|
||||||
|
pub mod auto_exposure;
|
||||||
pub mod blit;
|
pub mod blit;
|
||||||
pub mod bloom;
|
pub mod bloom;
|
||||||
pub mod contrast_adaptive_sharpening;
|
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;
|
return textureSampleLevel(dt_lut_texture, dt_lut_sampler, p, 0.0).rgb;
|
||||||
#else ifdef TONEMAP_METHOD_BLENDER_FILMIC
|
#else ifdef TONEMAP_METHOD_BLENDER_FILMIC
|
||||||
return textureSampleLevel(dt_lut_texture, dt_lut_sampler, p, 0.0).rgb;
|
return textureSampleLevel(dt_lut_texture, dt_lut_sampler, p, 0.0).rgb;
|
||||||
#else
|
#else
|
||||||
return vec3(1.0, 0.0, 1.0);
|
return vec3(1.0, 0.0, 1.0);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
@ -42,8 +42,8 @@ fn sample_current_lut(p: vec3<f32>) -> vec3<f32> {
|
|||||||
|
|
||||||
fn rgb_to_ycbcr(col: vec3<f32>) -> vec3<f32> {
|
fn rgb_to_ycbcr(col: vec3<f32>) -> vec3<f32> {
|
||||||
let m = mat3x3<f32>(
|
let m = mat3x3<f32>(
|
||||||
0.2126, 0.7152, 0.0722,
|
0.2126, 0.7152, 0.0722,
|
||||||
-0.1146, -0.3854, 0.5,
|
-0.1146, -0.3854, 0.5,
|
||||||
0.5, -0.4542, -0.0458
|
0.5, -0.4542, -0.0458
|
||||||
);
|
);
|
||||||
return col * m;
|
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> {
|
fn ycbcr_to_rgb(col: vec3<f32>) -> vec3<f32> {
|
||||||
let m = mat3x3<f32>(
|
let m = mat3x3<f32>(
|
||||||
1.0, 0.0, 1.5748,
|
1.0, 0.0, 1.5748,
|
||||||
1.0, -0.1873, -0.4681,
|
1.0, -0.1873, -0.4681,
|
||||||
1.0, 1.8556, 0.0
|
1.0, 1.8556, 0.0
|
||||||
);
|
);
|
||||||
return max(vec3(0.0), col * m);
|
return max(vec3(0.0), col * m);
|
||||||
@ -122,14 +122,14 @@ fn RRTAndODTFit(v: vec3<f32>) -> vec3<f32> {
|
|||||||
return a / b;
|
return a / b;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ACESFitted(color: vec3<f32>) -> vec3<f32> {
|
fn ACESFitted(color: vec3<f32>) -> vec3<f32> {
|
||||||
var fitted_color = color;
|
var fitted_color = color;
|
||||||
|
|
||||||
// sRGB => XYZ => D65_2_D60 => AP1 => RRT_SAT
|
// sRGB => XYZ => D65_2_D60 => AP1 => RRT_SAT
|
||||||
let rgb_to_rrt = mat3x3<f32>(
|
let rgb_to_rrt = mat3x3<f32>(
|
||||||
vec3(0.59719, 0.35458, 0.04823),
|
vec3(0.59719, 0.35458, 0.04823),
|
||||||
vec3(0.07600, 0.90834, 0.01566),
|
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
|
// 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 = vec3(r, g, b);
|
||||||
|
|
||||||
prepared_image = convertOpenDomainToNormalizedLog2_(prepared_image, -10.0, 6.5);
|
prepared_image = convertOpenDomainToNormalizedLog2_(prepared_image, -10.0, 6.5);
|
||||||
|
|
||||||
prepared_image = clamp(prepared_image, vec3(0.0), vec3(1.0));
|
prepared_image = clamp(prepared_image, vec3(0.0), vec3(1.0));
|
||||||
return prepared_image;
|
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.
|
// applies individually to shadows, midtones, and highlights.
|
||||||
#ifdef SECTIONAL_COLOR_GRADING
|
#ifdef SECTIONAL_COLOR_GRADING
|
||||||
color = sectional_color_grading(color, &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
|
#endif
|
||||||
|
|
||||||
// tone_mapping
|
// 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
|
#else ifdef TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM
|
||||||
color = somewhat_boring_display_transform(color.rgb);
|
color = somewhat_boring_display_transform(color.rgb);
|
||||||
#else ifdef TONEMAP_METHOD_TONY_MC_MAPFACE
|
#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
|
#else ifdef TONEMAP_METHOD_BLENDER_FILMIC
|
||||||
color = sample_blender_filmic_lut(color.rgb);
|
color = sample_blender_filmic_lut(color.rgb);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Perceptual post tonemapping grading
|
// Perceptual post tonemapping grading
|
||||||
color = saturation(color, color_grading.post_saturation);
|
color = saturation(color, color_grading.post_saturation);
|
||||||
|
|
||||||
return vec4(color, in.a);
|
return vec4(color, in.a);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -408,6 +408,15 @@ pub mod binding_types {
|
|||||||
.into_bind_group_layout_entry_builder()
|
.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 {
|
pub fn texture_2d(sample_type: TextureSampleType) -> BindGroupLayoutEntryBuilder {
|
||||||
BindingType::Texture {
|
BindingType::Texture {
|
||||||
sample_type,
|
sample_type,
|
||||||
|
@ -8,6 +8,8 @@ use encase::{
|
|||||||
};
|
};
|
||||||
use wgpu::{util::BufferInitDescriptor, BindingResource, BufferBinding, BufferUsages};
|
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.
|
/// 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.
|
/// 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.
|
/// 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
|
/// 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);
|
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::{
|
use wgpu::{
|
||||||
Extent3d, RenderPassColorAttachment, RenderPassDepthStencilAttachment, StoreOp,
|
BufferUsages, Extent3d, RenderPassColorAttachment, RenderPassDepthStencilAttachment, StoreOp,
|
||||||
TextureDescriptor, TextureDimension, TextureFormat, TextureUsages,
|
TextureDescriptor, TextureDimension, TextureFormat, TextureUsages,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -114,7 +114,7 @@ impl Plugin for ViewPlugin {
|
|||||||
));
|
));
|
||||||
|
|
||||||
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
|
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
|
||||||
render_app.init_resource::<ViewUniforms>().add_systems(
|
render_app.add_systems(
|
||||||
Render,
|
Render,
|
||||||
(
|
(
|
||||||
prepare_view_targets
|
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).
|
/// 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,
|
render_layers: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Resource, Default)]
|
#[derive(Resource)]
|
||||||
pub struct ViewUniforms {
|
pub struct ViewUniforms {
|
||||||
pub uniforms: DynamicUniformBuffer<ViewUniform>,
|
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)]
|
#[derive(Component)]
|
||||||
pub struct ViewUniformOffset {
|
pub struct ViewUniformOffset {
|
||||||
pub offset: u32,
|
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
|
[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
|
[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
|
[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
|
[Blend Modes](../examples/3d/blend_modes.rs) | Showcases different blend modes
|
||||||
[Color grading](../examples/3d/color_grading.rs) | Demonstrates color grading
|
[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
|
[Deferred Rendering](../examples/3d/deferred_rendering.rs) | Renders meshes with both forward and deferred pipelines
|
||||||
|
Loading…
Reference in New Issue
Block a user