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