bevy_post_process/auto_exposure/
compensation_curve.rs1use bevy_asset::{prelude::*, RenderAssetUsages};
2use bevy_curve::cubic_splines::CubicGenerator;
3use bevy_ecs::system::{lifetimeless::SRes, SystemParamItem};
4use bevy_math::{FloatExt, Vec2};
5use bevy_reflect::prelude::*;
6use bevy_render::{
7 render_asset::{AssetExtractionError, RenderAsset},
8 render_resource::{
9 Extent3d, ShaderType, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages,
10 TextureView, UniformBuffer,
11 },
12 renderer::{RenderDevice, RenderQueue},
13};
14use thiserror::Error;
15
16const LUT_SIZE: usize = 256;
17
18#[derive(Asset, Reflect, Debug, Clone)]
22#[reflect(Default, Clone)]
23pub struct AutoExposureCompensationCurve {
24 min_log_lum: f32,
26 max_log_lum: f32,
28 min_compensation: f32,
30 max_compensation: f32,
32 lut: [u8; LUT_SIZE],
41}
42
43#[derive(Error, Debug)]
45pub enum AutoExposureCompensationCurveError {
46 #[error("curve could not be constructed from the given data")]
48 InvalidCurve,
49 #[error("discontinuity found between curve segments")]
51 DiscontinuityFound,
52 #[error("curve is not monotonically increasing on the x-axis")]
54 NotMonotonic,
55}
56
57impl Default for AutoExposureCompensationCurve {
58 fn default() -> Self {
59 Self {
60 min_log_lum: 0.0,
61 max_log_lum: 0.0,
62 min_compensation: 0.0,
63 max_compensation: 0.0,
64 lut: [0; LUT_SIZE],
65 }
66 }
67}
68
69impl AutoExposureCompensationCurve {
70 const SAMPLES_PER_SEGMENT: usize = 64;
71
72 pub fn from_curve<T>(curve: T) -> Result<Self, AutoExposureCompensationCurveError>
103 where
104 T: CubicGenerator<Vec2>,
105 {
106 let Ok(curve) = curve.to_curve() else {
107 return Err(AutoExposureCompensationCurveError::InvalidCurve);
108 };
109
110 let min_log_lum = curve.position(0.0).x;
111 let max_log_lum = curve.position(curve.segments().len() as f32).x;
112 let log_lum_range = max_log_lum - min_log_lum;
113
114 let mut lut = [0.0; LUT_SIZE];
115
116 let mut previous = curve.position(0.0);
117 let mut min_compensation = previous.y;
118 let mut max_compensation = previous.y;
119
120 for segment in curve {
121 if segment.position(0.0) != previous {
122 return Err(AutoExposureCompensationCurveError::DiscontinuityFound);
123 }
124
125 for i in 1..Self::SAMPLES_PER_SEGMENT {
126 let current = segment.position(i as f32 / (Self::SAMPLES_PER_SEGMENT - 1) as f32);
127
128 if current.x < previous.x {
129 return Err(AutoExposureCompensationCurveError::NotMonotonic);
130 }
131
132 let (lut_begin, lut_end) = (
134 ((previous.x - min_log_lum) / log_lum_range) * (LUT_SIZE - 1) as f32,
135 ((current.x - min_log_lum) / log_lum_range) * (LUT_SIZE - 1) as f32,
136 );
137 let lut_inv_range = 1.0 / (lut_end - lut_begin);
138
139 #[expect(
141 clippy::needless_range_loop,
142 reason = "This for-loop also uses `i` to calculate a value `t`."
143 )]
144 for i in lut_begin.ceil() as usize..=lut_end.floor() as usize {
145 let t = (i as f32 - lut_begin) * lut_inv_range;
146 lut[i] = previous.y.lerp(current.y, t);
147 min_compensation = min_compensation.min(lut[i]);
148 max_compensation = max_compensation.max(lut[i]);
149 }
150
151 previous = current;
152 }
153 }
154
155 let compensation_range = max_compensation - min_compensation;
156
157 Ok(Self {
158 min_log_lum,
159 max_log_lum,
160 min_compensation,
161 max_compensation,
162 lut: if compensation_range > 0.0 {
163 let scale = 255.0 / compensation_range;
164 lut.map(|f: f32| ((f - min_compensation) * scale) as u8)
165 } else {
166 [0; LUT_SIZE]
167 },
168 })
169 }
170}
171
172pub struct GpuAutoExposureCompensationCurve {
176 pub(super) texture_view: TextureView,
177 pub(super) extents: UniformBuffer<AutoExposureCompensationCurveUniform>,
178}
179
180#[derive(ShaderType, Clone, Copy)]
181pub(super) struct AutoExposureCompensationCurveUniform {
182 min_log_lum: f32,
183 inv_log_lum_range: f32,
184 min_compensation: f32,
185 compensation_range: f32,
186}
187
188impl RenderAsset for GpuAutoExposureCompensationCurve {
189 type SourceAsset = AutoExposureCompensationCurve;
190 type Param = (SRes<RenderDevice>, SRes<RenderQueue>);
191
192 fn asset_usage(_: &Self::SourceAsset) -> RenderAssetUsages {
193 RenderAssetUsages::RENDER_WORLD
194 }
195
196 fn take_gpu_data(
197 source: &mut Self::SourceAsset,
198 _previous_gpu_asset: Option<&Self>,
199 ) -> Result<Self::SourceAsset, AssetExtractionError> {
200 Ok(source.clone())
201 }
202
203 fn prepare_asset(
204 source: Self::SourceAsset,
205 _: AssetId<Self::SourceAsset>,
206 (render_device, render_queue): &mut SystemParamItem<Self::Param>,
207 _: Option<&Self>,
208 ) -> Result<Self, bevy_render::render_asset::PrepareAssetError<Self::SourceAsset>> {
209 let texture = render_device.create_texture_with_data(
210 render_queue,
211 &TextureDescriptor {
212 label: None,
213 size: Extent3d {
214 width: LUT_SIZE as u32,
215 height: 1,
216 depth_or_array_layers: 1,
217 },
218 mip_level_count: 1,
219 sample_count: 1,
220 dimension: TextureDimension::D1,
221 format: TextureFormat::R8Unorm,
222 usage: TextureUsages::COPY_DST | TextureUsages::TEXTURE_BINDING,
223 view_formats: &[TextureFormat::R8Unorm],
224 },
225 Default::default(),
226 &source.lut,
227 );
228
229 let texture_view = texture.create_view(&Default::default());
230
231 let mut extents = UniformBuffer::from(AutoExposureCompensationCurveUniform {
232 min_log_lum: source.min_log_lum,
233 inv_log_lum_range: 1.0 / (source.max_log_lum - source.min_log_lum),
234 min_compensation: source.min_compensation,
235 compensation_range: source.max_compensation - source.min_compensation,
236 });
237
238 extents.write_buffer(render_device, render_queue);
239
240 Ok(GpuAutoExposureCompensationCurve {
241 texture_view,
242 extents,
243 })
244 }
245}