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}