bevy_core_pipeline/auto_exposure/
compensation_curve.rs1use bevy_asset::prelude::*;
2use bevy_ecs::system::{lifetimeless::SRes, SystemParamItem};
3use bevy_math::{cubic_splines::CubicGenerator, FloatExt, Vec2};
4use bevy_reflect::prelude::*;
5use bevy_render::{
6 render_asset::{RenderAsset, RenderAssetUsages},
7 render_resource::{
8 Extent3d, ShaderType, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages,
9 TextureView, UniformBuffer,
10 },
11 renderer::{RenderDevice, RenderQueue},
12};
13use thiserror::Error;
14
15const LUT_SIZE: usize = 256;
16
17#[derive(Asset, Reflect, Debug, Clone)]
21#[reflect(Default, Clone)]
22pub struct AutoExposureCompensationCurve {
23 min_log_lum: f32,
25 max_log_lum: f32,
27 min_compensation: f32,
29 max_compensation: f32,
31 lut: [u8; LUT_SIZE],
40}
41
42#[derive(Error, Debug)]
44pub enum AutoExposureCompensationCurveError {
45 #[error("curve could not be constructed from the given data")]
47 InvalidCurve,
48 #[error("discontinuity found between curve segments")]
50 DiscontinuityFound,
51 #[error("curve is not monotonically increasing on the x-axis")]
53 NotMonotonic,
54}
55
56impl 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
68impl AutoExposureCompensationCurve {
69 const SAMPLES_PER_SEGMENT: usize = 64;
70
71 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 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 #[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
171pub struct GpuAutoExposureCompensationCurve {
175 pub(super) texture_view: TextureView,
176 pub(super) extents: UniformBuffer<AutoExposureCompensationCurveUniform>,
177}
178
179#[derive(ShaderType, Clone, Copy)]
180pub(super) struct AutoExposureCompensationCurveUniform {
181 min_log_lum: f32,
182 inv_log_lum_range: f32,
183 min_compensation: f32,
184 compensation_range: f32,
185}
186
187impl 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 ) -> Result<Self, bevy_render::render_asset::PrepareAssetError<Self::SourceAsset>> {
200 let texture = render_device.create_texture_with_data(
201 render_queue,
202 &TextureDescriptor {
203 label: None,
204 size: Extent3d {
205 width: LUT_SIZE as u32,
206 height: 1,
207 depth_or_array_layers: 1,
208 },
209 mip_level_count: 1,
210 sample_count: 1,
211 dimension: TextureDimension::D1,
212 format: TextureFormat::R8Unorm,
213 usage: TextureUsages::COPY_DST | TextureUsages::TEXTURE_BINDING,
214 view_formats: &[TextureFormat::R8Unorm],
215 },
216 Default::default(),
217 &source.lut,
218 );
219
220 let texture_view = texture.create_view(&Default::default());
221
222 let mut extents = UniformBuffer::from(AutoExposureCompensationCurveUniform {
223 min_log_lum: source.min_log_lum,
224 inv_log_lum_range: 1.0 / (source.max_log_lum - source.min_log_lum),
225 min_compensation: source.min_compensation,
226 compensation_range: source.max_compensation - source.min_compensation,
227 });
228
229 extents.write_buffer(render_device, render_queue);
230
231 Ok(GpuAutoExposureCompensationCurve {
232 texture_view,
233 extents,
234 })
235 }
236}