gainforge/
mappers.rs

1/*
2 * // Copyright (c) Radzivon Bartoshyk 2/2025. All rights reserved.
3 * //
4 * // Redistribution and use in source and binary forms, with or without modification,
5 * // are permitted provided that the following conditions are met:
6 * //
7 * // 1.  Redistributions of source code must retain the above copyright notice, this
8 * // list of conditions and the following disclaimer.
9 * //
10 * // 2.  Redistributions in binary form must reproduce the above copyright notice,
11 * // this list of conditions and the following disclaimer in the documentation
12 * // and/or other materials provided with the distribution.
13 * //
14 * // 3.  Neither the name of the copyright holder nor the names of its
15 * // contributors may be used to endorse or promote products derived from
16 * // this software without specific prior written permission.
17 * //
18 * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19 * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20 * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22 * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23 * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24 * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25 * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26 * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27 * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 */
29use crate::mlaf::mlaf;
30use crate::spline::FilmicSplineParameters;
31use crate::GainHdrMetadata;
32use std::ops::{Add, Div, Mul, Sub};
33
34/// Defines a tone mapping method.
35///
36/// All tone mappers are local unless other is stated.
37///
38/// See [this blog post](https://64.github.io/tonemapping/) for more details on
39/// many of the supported tone mapping methods.
40#[derive(Debug, Copy, Clone, PartialOrd, PartialEq)]
41pub enum ToneMappingMethod {
42    /// ITU-R broadcasting TV [recommendation 2408](https://www.itu.int/dms_pub/itu-r/opb/rep/R-REP-BT.2408-4-2021-PDF-E.pdf)
43    Rec2408(GainHdrMetadata),
44    /// The ['Uncharted 2' filmic](https://www.gdcvault.com/play/1012351/Uncharted-2-HDR)
45    /// tone mapping method.
46    Filmic,
47    /// The [Academy Color Encoding System](https://github.com/ampas/aces-core)
48    /// filmic tone mapping method.
49    Aces,
50    /// Erik Reinhard's tone mapper from the paper "Photographic tone
51    /// reproduction for digital images".
52    Reinhard,
53    /// Same as `Reinhard` but scales the output to the full dynamic
54    /// range of the image.
55    ExtendedReinhard,
56    /// A variation of `Reinhard` that uses mixes color-based- with
57    /// luminance-based tone mapping.
58    ReinhardJodie,
59    /// Simply clamp the output to the available dynamic range.
60    Clamp,
61    /// This is a parameterized curve based on the Blender Filmic tone mapping
62    /// method similar to the module found in Ansel/Darktable.
63    FilmicSpline(FilmicSplineParameters),
64}
65
66pub(crate) trait ToneMap {
67    fn process_lane(&self, in_place: &mut [f32]);
68    /// This method always expect first item to be luma.
69    fn process_luma_lane(&self, in_place: &mut [f32]);
70}
71
72#[derive(Debug, Clone, Copy)]
73pub(crate) struct Rec2408ToneMapper<const CN: usize> {
74    w_a: f32,
75    w_b: f32,
76    primaries: [f32; 3],
77}
78
79impl<const CN: usize> Rec2408ToneMapper<CN> {
80    pub(crate) fn new(
81        content_max_brightness: f32,
82        display_max_brightness: f32,
83        white_point: f32,
84        primaries: [f32; 3],
85    ) -> Self {
86        let ld = content_max_brightness / white_point;
87        let w_a = (display_max_brightness / white_point) / (ld * ld);
88        let w_b = 1.0f32 / (display_max_brightness / white_point);
89        Self {
90            w_a,
91            w_b,
92            primaries,
93        }
94    }
95}
96
97impl<const CN: usize> Rec2408ToneMapper<CN> {
98    #[inline(always)]
99    fn tonemap(&self, luma: f32) -> f32 {
100        mlaf(1f32, self.w_a, luma) / mlaf(1f32, self.w_b, luma)
101    }
102}
103
104impl<const CN: usize> ToneMap for Rec2408ToneMapper<CN> {
105    fn process_lane(&self, in_place: &mut [f32]) {
106        for chunk in in_place.chunks_exact_mut(CN) {
107            let luma = chunk[0] * self.primaries[0]
108                + chunk[1] * self.primaries[1]
109                + chunk[2] * self.primaries[2];
110            if luma == 0. {
111                chunk[0] = 0.;
112                chunk[1] = 0.;
113                chunk[2] = 0.;
114                continue;
115            }
116            let scale = self.tonemap(luma);
117            chunk[0] = (chunk[0] * scale).min(1f32);
118            chunk[1] = (chunk[1] * scale).min(1f32);
119            chunk[2] = (chunk[2] * scale).min(1f32);
120        }
121    }
122
123    fn process_luma_lane(&self, in_place: &mut [f32]) {
124        for chunk in in_place.chunks_exact_mut(CN) {
125            let luma = chunk[0];
126            if luma == 0. {
127                chunk[0] = 0.;
128                continue;
129            }
130            let scale = self.tonemap(luma);
131            chunk[0] = (chunk[0] * scale).min(1f32);
132        }
133    }
134}
135
136#[derive(Debug, Clone, Copy, Default)]
137pub(crate) struct FilmicToneMapper<const CN: usize> {}
138
139#[inline(always)]
140const fn uncharted2_tonemap_partial(x: f32) -> f32 {
141    const A: f32 = 0.15f32;
142    const B: f32 = 0.50f32;
143    const C: f32 = 0.10f32;
144    const D: f32 = 0.20f32;
145    const E: f32 = 0.02f32;
146    const F: f32 = 0.30f32;
147    ((x * (A * x + C * B) + D * E) / (x * (A * x + B) + D * F)) - E / F
148}
149
150impl<const CN: usize> FilmicToneMapper<CN> {
151    #[inline(always)]
152    fn uncharted2_filmic(&self, v: f32) -> f32 {
153        let exposure_bias = 2.0f32;
154        let curr = uncharted2_tonemap_partial(v * exposure_bias);
155
156        const W: f32 = 11.2f32;
157        const W_S: f32 = 1.0f32 / uncharted2_tonemap_partial(W);
158        curr * W_S
159    }
160}
161
162impl<const CN: usize> ToneMap for FilmicToneMapper<CN> {
163    fn process_lane(&self, in_place: &mut [f32]) {
164        for chunk in in_place.chunks_exact_mut(CN) {
165            chunk[0] = self.uncharted2_filmic(chunk[0]).min(1f32);
166            chunk[1] = self.uncharted2_filmic(chunk[1]).min(1f32);
167            chunk[2] = self.uncharted2_filmic(chunk[2]).min(1f32);
168        }
169    }
170
171    fn process_luma_lane(&self, in_place: &mut [f32]) {
172        for chunk in in_place.chunks_exact_mut(CN) {
173            chunk[0] = self.uncharted2_filmic(chunk[0]).min(1f32);
174        }
175    }
176}
177
178#[derive(Debug, Clone, Copy, Default)]
179pub(crate) struct AcesToneMapper<const CN: usize> {}
180
181#[derive(Copy, Clone)]
182pub(crate) struct Rgb {
183    pub(crate) r: f32,
184    pub(crate) g: f32,
185    pub(crate) b: f32,
186}
187
188impl Mul<Rgb> for Rgb {
189    type Output = Self;
190    #[inline(always)]
191    fn mul(self, rhs: Rgb) -> Self::Output {
192        Self {
193            r: self.r * rhs.r,
194            g: self.g * rhs.g,
195            b: self.b * rhs.b,
196        }
197    }
198}
199
200impl Add<f32> for Rgb {
201    type Output = Self;
202    #[inline(always)]
203    fn add(self, rhs: f32) -> Self::Output {
204        Self {
205            r: self.r + rhs,
206            g: self.g + rhs,
207            b: self.b + rhs,
208        }
209    }
210}
211
212impl Sub<f32> for Rgb {
213    type Output = Self;
214    #[inline(always)]
215    fn sub(self, rhs: f32) -> Self::Output {
216        Self {
217            r: self.r - rhs,
218            g: self.g - rhs,
219            b: self.b - rhs,
220        }
221    }
222}
223
224impl Mul<f32> for Rgb {
225    type Output = Self;
226    #[inline(always)]
227    fn mul(self, rhs: f32) -> Self::Output {
228        Self {
229            r: self.r * rhs,
230            g: self.g * rhs,
231            b: self.b * rhs,
232        }
233    }
234}
235
236impl Div<f32> for Rgb {
237    type Output = Self;
238    #[inline(always)]
239    fn div(self, rhs: f32) -> Self::Output {
240        Self {
241            r: self.r / rhs,
242            g: self.g / rhs,
243            b: self.b / rhs,
244        }
245    }
246}
247
248impl Div<Rgb> for Rgb {
249    type Output = Self;
250    #[inline(always)]
251    fn div(self, rhs: Rgb) -> Self::Output {
252        Self {
253            r: self.r / rhs.r,
254            g: self.g / rhs.g,
255            b: self.b / rhs.b,
256        }
257    }
258}
259
260impl<const CN: usize> AcesToneMapper<CN> {
261    #[inline(always)]
262    fn mul_input(&self, color: Rgb) -> Rgb {
263        let a = mlaf(
264            mlaf(0.35458f32 * color.g, 0.04823f32, color.b),
265            0.59719f32,
266            color.r,
267        );
268        let b = mlaf(
269            mlaf(0.07600f32 * color.r, 0.90834f32, color.g),
270            0.01566f32,
271            color.b,
272        );
273        let c = mlaf(
274            mlaf(0.02840f32 * color.r, 0.13383f32, color.g),
275            0.83777f32,
276            color.b,
277        );
278        Rgb { r: a, g: b, b: c }
279    }
280
281    #[inline(always)]
282    fn mul_output(&self, color: Rgb) -> Rgb {
283        let a = mlaf(
284            mlaf(1.60475f32 * color.r, -0.53108f32, color.g),
285            -0.07367f32,
286            color.b,
287        );
288        let b = mlaf(
289            mlaf(-0.10208f32 * color.r, 1.10813f32, color.g),
290            -0.00605f32,
291            color.b,
292        );
293        let c = mlaf(
294            mlaf(-0.00327f32 * color.r, -0.07276f32, color.g),
295            1.07602f32,
296            color.b,
297        );
298        Rgb { r: a, g: b, b: c }
299    }
300}
301
302impl<const CN: usize> ToneMap for AcesToneMapper<CN> {
303    fn process_lane(&self, in_place: &mut [f32]) {
304        for chunk in in_place.chunks_exact_mut(CN) {
305            let color_in = self.mul_input(Rgb {
306                r: chunk[0],
307                g: chunk[1],
308                b: chunk[2],
309            });
310            let ca = color_in * (color_in + 0.0245786f32) - 0.000090537f32;
311            let cb = color_in * (color_in * 0.983729f32 + 0.4329510f32) + 0.238081f32;
312            let c_out = self.mul_output(ca / cb);
313            chunk[0] = c_out.r.min(1f32);
314            chunk[1] = c_out.g.min(1f32);
315            chunk[2] = c_out.b.min(1f32);
316        }
317    }
318
319    fn process_luma_lane(&self, in_place: &mut [f32]) {
320        for chunk in in_place.chunks_exact_mut(CN) {
321            let color_in = self.mul_input(Rgb {
322                r: chunk[0],
323                g: chunk[1],
324                b: chunk[2],
325            });
326            let ca = color_in * (color_in + 0.0245786f32) - 0.000090537f32;
327            let cb = color_in * (color_in * 0.983729f32 + 0.4329510f32) + 0.238081f32;
328            let c_out = self.mul_output(ca / cb);
329            chunk[0] = c_out.r.min(1f32);
330        }
331    }
332}
333
334#[derive(Debug, Clone, Copy, Default)]
335pub(crate) struct ReinhardToneMapper<const CN: usize> {}
336
337impl<const CN: usize> ToneMap for ReinhardToneMapper<CN> {
338    fn process_lane(&self, in_place: &mut [f32]) {
339        for chunk in in_place.chunks_exact_mut(CN) {
340            chunk[0] = (chunk[0] / (1f32 + chunk[0])).min(1f32);
341            chunk[1] = (chunk[1] / (1f32 + chunk[1])).min(1f32);
342            chunk[2] = (chunk[2] / (1f32 + chunk[2])).min(1f32);
343        }
344    }
345
346    fn process_luma_lane(&self, in_place: &mut [f32]) {
347        for chunk in in_place.chunks_exact_mut(CN) {
348            chunk[0] = (chunk[0] / (1f32 + chunk[0])).min(1f32);
349        }
350    }
351}
352
353#[derive(Debug, Clone, Copy, Default)]
354pub(crate) struct ExtendedReinhardToneMapper<const CN: usize> {
355    pub(crate) primaries: [f32; 3],
356}
357
358impl<const CN: usize> ToneMap for ExtendedReinhardToneMapper<CN> {
359    fn process_lane(&self, in_place: &mut [f32]) {
360        for chunk in in_place.chunks_exact_mut(CN) {
361            let luma = chunk[0] * self.primaries[0]
362                + chunk[1] * self.primaries[1]
363                + chunk[2] * self.primaries[2];
364            if luma == 0. {
365                chunk[0] = 0.;
366                chunk[1] = 0.;
367                chunk[2] = 0.;
368                continue;
369            }
370            let new_luma = luma / (1f32 + luma);
371            chunk[0] = (chunk[0] * new_luma).min(1f32);
372            chunk[1] = (chunk[1] * new_luma).min(1f32);
373            chunk[2] = (chunk[2] * new_luma).min(1f32);
374        }
375    }
376
377    fn process_luma_lane(&self, in_place: &mut [f32]) {
378        for chunk in in_place.chunks_exact_mut(CN) {
379            let luma = chunk[0];
380            if luma == 0. {
381                chunk[0] = 0.;
382                continue;
383            }
384            let new_luma = luma / (1f32 + luma);
385            chunk[0] = (chunk[0] * new_luma).min(1f32);
386        }
387    }
388}
389
390#[inline(always)]
391fn lerp(a: f32, b: f32, t: f32) -> f32 {
392    mlaf(a, t, b - a)
393}
394
395#[derive(Debug, Clone, Copy, Default)]
396pub(crate) struct ReinhardJodieToneMapper<const CN: usize> {
397    pub(crate) primaries: [f32; 3],
398}
399
400impl<const CN: usize> ToneMap for ReinhardJodieToneMapper<CN> {
401    fn process_lane(&self, in_place: &mut [f32]) {
402        for chunk in in_place.chunks_exact_mut(CN) {
403            let luma = chunk[0] * self.primaries[0]
404                + chunk[1] * self.primaries[1]
405                + chunk[2] * self.primaries[2];
406            if luma == 0. {
407                chunk[0] = 0.;
408                chunk[1] = 0.;
409                chunk[2] = 0.;
410                continue;
411            }
412            let tv_r = chunk[0] / (1.0f32 + chunk[0]);
413            let tv_g = chunk[1] / (1.0f32 + chunk[1]);
414            let tv_b = chunk[2] / (1.0f32 + chunk[2]);
415
416            chunk[0] = lerp(chunk[0] / (1f32 + luma), tv_r, tv_r).min(1f32);
417            chunk[1] = lerp(chunk[1] / (1f32 + luma), tv_g, tv_g).min(1f32);
418            chunk[2] = lerp(chunk[1] / (1f32 + luma), tv_b, tv_b).min(1f32);
419        }
420    }
421
422    fn process_luma_lane(&self, in_place: &mut [f32]) {
423        for chunk in in_place.chunks_exact_mut(CN) {
424            let luma = chunk[0];
425            if luma == 0. {
426                chunk[0] = 0.;
427                continue;
428            }
429            let tv_r = chunk[0] / (1.0f32 + chunk[0]);
430
431            chunk[0] = lerp(chunk[0] / (1f32 + luma), tv_r, tv_r).min(1f32);
432        }
433    }
434}
435
436#[derive(Debug, Clone, Copy, Default)]
437pub(crate) struct ClampToneMapper<const CN: usize> {}
438
439impl<const CN: usize> ToneMap for ClampToneMapper<CN> {
440    fn process_lane(&self, in_place: &mut [f32]) {
441        for chunk in in_place.chunks_exact_mut(CN) {
442            chunk[0] = chunk[0].min(1f32).max(0f32);
443            chunk[1] = chunk[1].min(1f32).max(0f32);
444            chunk[2] = chunk[2].min(1f32).max(0f32);
445        }
446    }
447
448    fn process_luma_lane(&self, in_place: &mut [f32]) {
449        for chunk in in_place.chunks_exact_mut(CN) {
450            chunk[0] = chunk[0].min(1f32).max(0f32);
451        }
452    }
453}