Skip to main content

pfx_post/
bloom.rs

1use crate::grade::{self, Downsample, keep_rgb};
2use crate::image::Image;
3
4pub const RING_SAMPLES: u32 = 20;
5pub const RING_TURN: f32 = 0.37;
6
7pub const NEUTRAL_RADII: [f32; 4] = [1.0, 2.0, 4.0, 8.0];
8pub const NEUTRAL_GAINS: [f32; 4] = [1.0, 0.5, 0.25, 0.125];
9
10#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub enum BloomKind {
12    Box,
13    Tent,
14    Rings,
15}
16
17#[derive(Clone, Copy, Debug)]
18pub struct Bloom {
19    pub kind: BloomKind,
20    pub strength: f32,
21    pub threshold: f32,
22    pub knee: f32,
23    pub knee_width: f32,
24    pub soft: f32,
25    pub radius: u32,
26    pub spread: f32,
27    pub down: u32,
28    pub clamp: f32,
29    pub unit: f32,
30    pub radii: [f32; 4],
31    pub gains: [f32; 4],
32    pub linear_taps: bool,
33}
34
35impl Bloom {
36    pub fn neutral(strength: f32) -> Self {
37        Self::pyramid(strength, 1.0, 0.0, 0.0, 0.0, 4, 16.0, 2)
38    }
39
40    #[allow(clippy::too_many_arguments)]
41    pub fn pyramid(
42        strength: f32,
43        threshold: f32,
44        knee: f32,
45        knee_width: f32,
46        soft: f32,
47        radius: u32,
48        spread: f32,
49        down: u32,
50    ) -> Self {
51        Self {
52            kind: BloomKind::Box,
53            strength,
54            threshold,
55            knee,
56            knee_width,
57            soft,
58            radius,
59            spread,
60            down,
61            clamp: 4.0,
62            unit: 1.0,
63            radii: NEUTRAL_RADII,
64            gains: NEUTRAL_GAINS,
65            linear_taps: true,
66        }
67    }
68
69    pub fn tent(strength: f32, threshold: f32, radius: u32, spread: f32, down: u32) -> Self {
70        Self {
71            kind: BloomKind::Tent,
72            strength,
73            threshold,
74            knee: 0.0,
75            knee_width: 0.0,
76            soft: 0.0,
77            radius,
78            spread,
79            down,
80            clamp: 4.0,
81            unit: 1.0,
82            radii: NEUTRAL_RADII,
83            gains: NEUTRAL_GAINS,
84            linear_taps: false,
85        }
86    }
87
88    pub fn rings(
89        strength: f32,
90        threshold: f32,
91        down: u32,
92        radii: [f32; 4],
93        gains: [f32; 4],
94    ) -> Self {
95        Self {
96            kind: BloomKind::Rings,
97            strength,
98            threshold,
99            knee: 0.0,
100            knee_width: 0.0,
101            soft: 0.0,
102            radius: 0,
103            spread: 1.0,
104            down,
105            clamp: 4.0,
106            unit: 1.0,
107            radii,
108            gains,
109            linear_taps: false,
110        }
111    }
112}
113
114pub fn apply(image: &Image, bloom: &Bloom) -> Image {
115    if bloom.strength.abs() < 1e-8 {
116        return image.clone();
117    }
118    match bloom.kind {
119        BloomKind::Rings => rings(image, bloom),
120        BloomKind::Box | BloomKind::Tent => pyramid(image, bloom),
121    }
122}
123
124fn pyramid(image: &Image, bloom: &Bloom) -> Image {
125    let filter = if bloom.kind == BloomKind::Tent {
126        Downsample::Tent
127    } else {
128        Downsample::Box
129    };
130    let small = grade::downsample(image, bloom.down.max(1), filter);
131    let bright = small
132        .map_rgb(|_, _, c| keep_rgb(c, bloom.threshold, bloom.knee, bloom.knee_width, bloom.soft));
133    let blurred = grade::separable(&bright, bloom.radius, bloom.spread);
134    add(image, &blurred, bloom.strength)
135}
136
137pub fn add(image: &Image, bloom: &Image, strength: f32) -> Image {
138    let bw = bloom.width as f32;
139    let bh = bloom.height as f32;
140    let w = image.width.max(1) as f32;
141    let h = image.height.max(1) as f32;
142    let mut out = image.clone();
143    for y in 0..image.height {
144        for x in 0..image.width {
145            let u = (x as f32 + 0.5) / w;
146            let v = (y as f32 + 0.5) / h;
147            let s = bloom.sample(u * bw - 0.5, v * bh - 0.5);
148            let i = image.index(x, y);
149            let p = out.pixels[i];
150            out.pixels[i] = [
151                p[0] + s[0] * strength,
152                p[1] + s[1] * strength,
153                p[2] + s[2] * strength,
154                p[3],
155            ];
156        }
157    }
158    out
159}
160
161fn rings(image: &Image, bloom: &Bloom) -> Image {
162    let factor = bloom.down.max(1);
163    let small = grade::downsample(image, factor, Downsample::Tent).map_rgb(|_, _, c| {
164        std::array::from_fn(|ch| {
165            let value = (c[ch] - bloom.threshold).max(0.0);
166            if bloom.clamp > 0.0 {
167                value.min(bloom.clamp)
168            } else {
169                value
170            }
171        })
172    });
173    let mut blurred = small.clone();
174    for y in 0..small.height {
175        for x in 0..small.width {
176            let mut acc = [0.0; 3];
177            let mut weight = 0.0;
178            for ring in 0..4 {
179                let turn = ring as f32 * RING_TURN;
180                let radius = bloom.radii[ring] * bloom.unit / factor as f32;
181                let gain = bloom.gains[ring];
182                for i in 0..RING_SAMPLES {
183                    let a = i as f32 * std::f32::consts::TAU / RING_SAMPLES as f32 + turn;
184                    let s = small.sample(x as f32 + a.cos() * radius, y as f32 + a.sin() * radius);
185                    for ch in 0..3 {
186                        acc[ch] += s[ch] * gain;
187                    }
188                }
189                weight += RING_SAMPLES as f32 * gain;
190            }
191            if weight > 1e-8 {
192                let i = small.index(x, y);
193                blurred.pixels[i] = [acc[0] / weight, acc[1] / weight, acc[2] / weight, 1.0];
194            }
195        }
196    }
197    add(image, &blurred, bloom.strength)
198}
199
200pub fn linear_taps(radius: u32, spread: f32) -> (f64, Vec<(f64, f64)>) {
201    let radius = radius.min(64);
202    let spread = if spread <= 1e-4 {
203        1.0
204    } else {
205        f64::from(spread)
206    };
207    let raw: Vec<f64> = (0..=radius)
208        .map(|k| (-f64::from(k * k) / spread).exp())
209        .collect();
210    let total = raw[0] + 2.0 * raw[1..].iter().sum::<f64>();
211    let mut taps = Vec::new();
212    let mut k = 1;
213    while k <= radius as usize {
214        if k < radius as usize {
215            let (left, right) = (raw[k], raw[k + 1]);
216            let sum = left + right;
217            taps.push((
218                sum / total,
219                (k as f64 * left + (k + 1) as f64 * right) / sum,
220            ));
221            k += 2;
222        } else {
223            taps.push((raw[k] / total, k as f64));
224            k += 1;
225        }
226    }
227    (raw[0] / total, taps)
228}