Path: blob/main/crates/bevy_post_process/src/auto_exposure/compensation_curve.rs
6596 views
use bevy_asset::{prelude::*, RenderAssetUsages};1use bevy_ecs::system::{lifetimeless::SRes, SystemParamItem};2use bevy_math::{cubic_splines::CubicGenerator, FloatExt, Vec2};3use bevy_reflect::prelude::*;4use bevy_render::{5render_asset::RenderAsset,6render_resource::{7Extent3d, ShaderType, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages,8TextureView, UniformBuffer,9},10renderer::{RenderDevice, RenderQueue},11};12use thiserror::Error;1314const LUT_SIZE: usize = 256;1516/// An auto exposure compensation curve.17/// This curve is used to map the average log luminance of a scene to an18/// exposure compensation value, to allow for fine control over the final exposure.19#[derive(Asset, Reflect, Debug, Clone)]20#[reflect(Default, Clone)]21pub struct AutoExposureCompensationCurve {22/// The minimum log luminance value in the curve. (the x-axis)23min_log_lum: f32,24/// The maximum log luminance value in the curve. (the x-axis)25max_log_lum: f32,26/// The minimum exposure compensation value in the curve. (the y-axis)27min_compensation: f32,28/// The maximum exposure compensation value in the curve. (the y-axis)29max_compensation: f32,30/// The lookup table for the curve. Uploaded to the GPU as a 1D texture.31/// Each value in the LUT is a `u8` representing a normalized exposure compensation value:32/// * `0` maps to `min_compensation`33/// * `255` maps to `max_compensation`34///35/// The position in the LUT corresponds to the normalized log luminance value.36/// * `0` maps to `min_log_lum`37/// * `LUT_SIZE - 1` maps to `max_log_lum`38lut: [u8; LUT_SIZE],39}4041/// Various errors that can occur when constructing an [`AutoExposureCompensationCurve`].42#[derive(Error, Debug)]43pub enum AutoExposureCompensationCurveError {44/// The curve couldn't be built in the first place.45#[error("curve could not be constructed from the given data")]46InvalidCurve,47/// A discontinuity was found in the curve.48#[error("discontinuity found between curve segments")]49DiscontinuityFound,50/// The curve is not monotonically increasing on the x-axis.51#[error("curve is not monotonically increasing on the x-axis")]52NotMonotonic,53}5455impl Default for AutoExposureCompensationCurve {56fn default() -> Self {57Self {58min_log_lum: 0.0,59max_log_lum: 0.0,60min_compensation: 0.0,61max_compensation: 0.0,62lut: [0; LUT_SIZE],63}64}65}6667impl AutoExposureCompensationCurve {68const SAMPLES_PER_SEGMENT: usize = 64;6970/// Build an [`AutoExposureCompensationCurve`] from a [`CubicGenerator<Vec2>`], where:71/// - x represents the average log luminance of the scene in EV-100;72/// - y represents the exposure compensation value in F-stops.73///74/// # Errors75///76/// If the curve is not monotonically increasing on the x-axis,77/// returns [`AutoExposureCompensationCurveError::NotMonotonic`].78///79/// If a discontinuity is found between curve segments,80/// returns [`AutoExposureCompensationCurveError::DiscontinuityFound`].81///82/// # Example83///84/// ```85/// # use bevy_asset::prelude::*;86/// # use bevy_math::vec2;87/// # use bevy_math::cubic_splines::*;88/// # use bevy_post_process::auto_exposure::AutoExposureCompensationCurve;89/// # let mut compensation_curves = Assets::<AutoExposureCompensationCurve>::default();90/// let curve: Handle<AutoExposureCompensationCurve> = compensation_curves.add(91/// AutoExposureCompensationCurve::from_curve(LinearSpline::new([92/// vec2(-4.0, -2.0),93/// vec2(0.0, 0.0),94/// vec2(2.0, 0.0),95/// vec2(4.0, 2.0),96/// ]))97/// .unwrap()98/// );99/// ```100pub fn from_curve<T>(curve: T) -> Result<Self, AutoExposureCompensationCurveError>101where102T: CubicGenerator<Vec2>,103{104let Ok(curve) = curve.to_curve() else {105return Err(AutoExposureCompensationCurveError::InvalidCurve);106};107108let min_log_lum = curve.position(0.0).x;109let max_log_lum = curve.position(curve.segments().len() as f32).x;110let log_lum_range = max_log_lum - min_log_lum;111112let mut lut = [0.0; LUT_SIZE];113114let mut previous = curve.position(0.0);115let mut min_compensation = previous.y;116let mut max_compensation = previous.y;117118for segment in curve {119if segment.position(0.0) != previous {120return Err(AutoExposureCompensationCurveError::DiscontinuityFound);121}122123for i in 1..Self::SAMPLES_PER_SEGMENT {124let current = segment.position(i as f32 / (Self::SAMPLES_PER_SEGMENT - 1) as f32);125126if current.x < previous.x {127return Err(AutoExposureCompensationCurveError::NotMonotonic);128}129130// Find the range of LUT entries that this line segment covers.131let (lut_begin, lut_end) = (132((previous.x - min_log_lum) / log_lum_range) * (LUT_SIZE - 1) as f32,133((current.x - min_log_lum) / log_lum_range) * (LUT_SIZE - 1) as f32,134);135let lut_inv_range = 1.0 / (lut_end - lut_begin);136137// Iterate over all LUT entries whose pixel centers fall within the current segment.138#[expect(139clippy::needless_range_loop,140reason = "This for-loop also uses `i` to calculate a value `t`."141)]142for i in lut_begin.ceil() as usize..=lut_end.floor() as usize {143let t = (i as f32 - lut_begin) * lut_inv_range;144lut[i] = previous.y.lerp(current.y, t);145min_compensation = min_compensation.min(lut[i]);146max_compensation = max_compensation.max(lut[i]);147}148149previous = current;150}151}152153let compensation_range = max_compensation - min_compensation;154155Ok(Self {156min_log_lum,157max_log_lum,158min_compensation,159max_compensation,160lut: if compensation_range > 0.0 {161let scale = 255.0 / compensation_range;162lut.map(|f: f32| ((f - min_compensation) * scale) as u8)163} else {164[0; LUT_SIZE]165},166})167}168}169170/// The GPU-representation of an [`AutoExposureCompensationCurve`].171/// Consists of a [`TextureView`] with the curve's data,172/// and a [`UniformBuffer`] with the curve's extents.173pub struct GpuAutoExposureCompensationCurve {174pub(super) texture_view: TextureView,175pub(super) extents: UniformBuffer<AutoExposureCompensationCurveUniform>,176}177178#[derive(ShaderType, Clone, Copy)]179pub(super) struct AutoExposureCompensationCurveUniform {180min_log_lum: f32,181inv_log_lum_range: f32,182min_compensation: f32,183compensation_range: f32,184}185186impl RenderAsset for GpuAutoExposureCompensationCurve {187type SourceAsset = AutoExposureCompensationCurve;188type Param = (SRes<RenderDevice>, SRes<RenderQueue>);189190fn asset_usage(_: &Self::SourceAsset) -> RenderAssetUsages {191RenderAssetUsages::RENDER_WORLD192}193194fn prepare_asset(195source: Self::SourceAsset,196_: AssetId<Self::SourceAsset>,197(render_device, render_queue): &mut SystemParamItem<Self::Param>,198_: Option<&Self>,199) -> Result<Self, bevy_render::render_asset::PrepareAssetError<Self::SourceAsset>> {200let texture = render_device.create_texture_with_data(201render_queue,202&TextureDescriptor {203label: None,204size: Extent3d {205width: LUT_SIZE as u32,206height: 1,207depth_or_array_layers: 1,208},209mip_level_count: 1,210sample_count: 1,211dimension: TextureDimension::D1,212format: TextureFormat::R8Unorm,213usage: TextureUsages::COPY_DST | TextureUsages::TEXTURE_BINDING,214view_formats: &[TextureFormat::R8Unorm],215},216Default::default(),217&source.lut,218);219220let texture_view = texture.create_view(&Default::default());221222let mut extents = UniformBuffer::from(AutoExposureCompensationCurveUniform {223min_log_lum: source.min_log_lum,224inv_log_lum_range: 1.0 / (source.max_log_lum - source.min_log_lum),225min_compensation: source.min_compensation,226compensation_range: source.max_compensation - source.min_compensation,227});228229extents.write_buffer(render_device, render_queue);230231Ok(GpuAutoExposureCompensationCurve {232texture_view,233extents,234})235}236}237238239