bevy_core_pipeline/auto_exposure/compensation_curve.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232
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 derive_more::derive::{Display, 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, Display, Debug)]
pub enum AutoExposureCompensationCurveError {
/// The curve couldn't be built in the first place.
#[display("curve could not be constructed from the given data")]
InvalidCurve,
/// A discontinuity was found in the curve.
#[display("discontinuity found between curve segments")]
DiscontinuityFound,
/// The curve is not monotonically increasing on the x-axis.
#[display("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 Ok(curve) = curve.to_curve() else {
return Err(AutoExposureCompensationCurveError::InvalidCurve);
};
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,
})
}
}