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 derive_more::derive::{Display, Error};
14
15const LUT_SIZE: usize = 256;
16
17#[derive(Asset, Reflect, Debug, Clone)]
21#[reflect(Default)]
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, Display, Debug)]
44pub enum AutoExposureCompensationCurveError {
45 #[display("curve could not be constructed from the given data")]
47 InvalidCurve,
48 #[display("discontinuity found between curve segments")]
50 DiscontinuityFound,
51 #[display("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 #[allow(clippy::needless_range_loop)]
140 for i in lut_begin.ceil() as usize..=lut_end.floor() as usize {
141 let t = (i as f32 - lut_begin) * lut_inv_range;
142 lut[i] = previous.y.lerp(current.y, t);
143 min_compensation = min_compensation.min(lut[i]);
144 max_compensation = max_compensation.max(lut[i]);
145 }
146
147 previous = current;
148 }
149 }
150
151 let compensation_range = max_compensation - min_compensation;
152
153 Ok(Self {
154 min_log_lum,
155 max_log_lum,
156 min_compensation,
157 max_compensation,
158 lut: if compensation_range > 0.0 {
159 let scale = 255.0 / compensation_range;
160 lut.map(|f: f32| ((f - min_compensation) * scale) as u8)
161 } else {
162 [0; LUT_SIZE]
163 },
164 })
165 }
166}
167
168pub struct GpuAutoExposureCompensationCurve {
172 pub(super) texture_view: TextureView,
173 pub(super) extents: UniformBuffer<AutoExposureCompensationCurveUniform>,
174}
175
176#[derive(ShaderType, Clone, Copy)]
177pub(super) struct AutoExposureCompensationCurveUniform {
178 min_log_lum: f32,
179 inv_log_lum_range: f32,
180 min_compensation: f32,
181 compensation_range: f32,
182}
183
184impl RenderAsset for GpuAutoExposureCompensationCurve {
185 type SourceAsset = AutoExposureCompensationCurve;
186 type Param = (SRes<RenderDevice>, SRes<RenderQueue>);
187
188 fn asset_usage(_: &Self::SourceAsset) -> RenderAssetUsages {
189 RenderAssetUsages::RENDER_WORLD
190 }
191
192 fn prepare_asset(
193 source: Self::SourceAsset,
194 (render_device, render_queue): &mut SystemParamItem<Self::Param>,
195 ) -> Result<Self, bevy_render::render_asset::PrepareAssetError<Self::SourceAsset>> {
196 let texture = render_device.create_texture_with_data(
197 render_queue,
198 &TextureDescriptor {
199 label: None,
200 size: Extent3d {
201 width: LUT_SIZE as u32,
202 height: 1,
203 depth_or_array_layers: 1,
204 },
205 mip_level_count: 1,
206 sample_count: 1,
207 dimension: TextureDimension::D1,
208 format: TextureFormat::R8Unorm,
209 usage: TextureUsages::COPY_DST | TextureUsages::TEXTURE_BINDING,
210 view_formats: &[TextureFormat::R8Unorm],
211 },
212 Default::default(),
213 &source.lut,
214 );
215
216 let texture_view = texture.create_view(&Default::default());
217
218 let mut extents = UniformBuffer::from(AutoExposureCompensationCurveUniform {
219 min_log_lum: source.min_log_lum,
220 inv_log_lum_range: 1.0 / (source.max_log_lum - source.min_log_lum),
221 min_compensation: source.min_compensation,
222 compensation_range: source.max_compensation - source.min_compensation,
223 });
224
225 extents.write_buffer(render_device, render_queue);
226
227 Ok(GpuAutoExposureCompensationCurve {
228 texture_view,
229 extents,
230 })
231 }
232}