1use crate::color::{self, REC709};
2use crate::hash::{self, grain_hash};
3use crate::image::{self, Image};
4use crate::tone::Tone;
5
6#[derive(Clone, Copy, Debug)]
7pub struct Saturation {
8 pub amount: f32,
9 pub weights: [f32; 3],
10}
11
12impl Saturation {
13 pub fn rec709(amount: f32) -> Self {
14 Self {
15 amount,
16 weights: REC709,
17 }
18 }
19}
20
21#[derive(Clone, Copy, Debug)]
22pub enum Contrast {
23 Pivot { amount: f32, pivot: f32 },
24 Power { power: f32, lift: f32 },
25}
26
27#[derive(Clone, Copy, Debug, PartialEq, Eq)]
28pub enum WarmthKind {
29 Linear,
30 LumaCurve,
31}
32
33#[derive(Clone, Copy, Debug)]
34pub struct Warmth {
35 pub amount: f32,
36 pub kind: WarmthKind,
37 pub low: [f32; 2],
38 pub high: [f32; 2],
39 pub shadow: f32,
40 pub highlight: f32,
41}
42
43impl Warmth {
44 pub fn linear(amount: f32) -> Self {
45 Self {
46 amount,
47 kind: WarmthKind::Linear,
48 low: [1.0; 2],
49 high: [1.0; 2],
50 shadow: 0.0,
51 highlight: 1.0,
52 }
53 }
54
55 pub fn luma_curve(
56 amount: f32,
57 low: [f32; 2],
58 high: [f32; 2],
59 shadow: f32,
60 highlight: f32,
61 ) -> Self {
62 Self {
63 amount,
64 kind: WarmthKind::LumaCurve,
65 low,
66 high,
67 shadow,
68 highlight,
69 }
70 }
71}
72
73#[derive(Clone, Debug)]
74pub struct Lut {
75 pub size: u32,
76 pub values: Vec<[f32; 3]>,
77}
78
79impl Lut {
80 pub fn sample(&self, c: [f32; 3]) -> [f32; 3] {
81 let n = self.size;
82 let need = (n as usize)
83 .saturating_mul(n as usize)
84 .saturating_mul(n as usize);
85 if n < 2 || self.values.len() < need {
86 return c;
87 }
88 let scale = (n - 1) as f32;
89 let coord = |v: f32| {
90 let x = v.clamp(0.0, 1.0) * scale;
91 let i = (x.floor() as u32).min(n - 1);
92 let j = (i + 1).min(n - 1);
93 (i, j, x - i as f32)
94 };
95 let (x0, x1, tx) = coord(c[0]);
96 let (y0, y1, ty) = coord(c[1]);
97 let (z0, z1, tz) = coord(c[2]);
98 let at = |x, y, z| self.values[(x + n * (y + n * z)) as usize];
99 let mix = |a: [f32; 3], b: [f32; 3], t: f32| color::lerp3(a, b, t);
100 let c00 = mix(at(x0, y0, z0), at(x1, y0, z0), tx);
101 let c01 = mix(at(x0, y0, z1), at(x1, y0, z1), tx);
102 let c10 = mix(at(x0, y1, z0), at(x1, y1, z0), tx);
103 let c11 = mix(at(x0, y1, z1), at(x1, y1, z1), tx);
104 let c0 = mix(c00, c10, ty);
105 let c1 = mix(c01, c11, ty);
106 mix(c0, c1, tz)
107 }
108}
109
110#[derive(Clone, Copy, Debug, PartialEq, Eq)]
111pub enum VignetteKind {
112 Power,
113 Smoothstep,
114}
115
116#[derive(Clone, Copy, Debug)]
117pub struct Vignette {
118 pub strength: f32,
119 pub kind: VignetteKind,
120 pub power: f32,
121 pub aspect: f32,
122 pub scale: f32,
123 pub inner: f32,
124 pub outer: f32,
125}
126
127impl Vignette {
128 pub fn power(strength: f32, power: f32, aspect: f32, scale: f32) -> Self {
129 Self {
130 strength,
131 kind: VignetteKind::Power,
132 power,
133 aspect,
134 scale,
135 inner: 0.0,
136 outer: 1.0,
137 }
138 }
139
140 pub fn smoothstep(strength: f32, aspect: f32, inner: f32, outer: f32) -> Self {
141 Self {
142 strength,
143 kind: VignetteKind::Smoothstep,
144 power: 1.0,
145 aspect,
146 scale: 1.0,
147 inner,
148 outer,
149 }
150 }
151}
152
153#[derive(Clone, Copy, Debug)]
154pub struct Grain {
155 pub strength: f32,
156 pub response: f32,
157 pub clamp: bool,
158}
159
160impl Grain {
161 pub fn clamped(strength: f32, response: f32) -> Self {
162 Self {
163 strength,
164 response,
165 clamp: true,
166 }
167 }
168}
169
170#[derive(Clone, Copy, Debug)]
171pub struct Dither {
172 pub amplitude: f32,
173}
174
175pub fn exposure(image: &Image, amount: f32) -> Image {
176 if (amount - 1.0).abs() < 1e-8 {
177 return image.clone();
178 }
179 image.map_rgb(|_, _, c| [c[0] * amount, c[1] * amount, c[2] * amount])
180}
181
182pub fn black(image: &Image, black: f32) -> Image {
183 let denom = 1.0 - black;
184 image.map_rgb(|_, _, c| {
185 if denom.abs() < 1e-6 {
186 [0.0; 3]
187 } else {
188 [
189 ((c[0] - black) / denom).max(0.0),
190 ((c[1] - black) / denom).max(0.0),
191 ((c[2] - black) / denom).max(0.0),
192 ]
193 }
194 })
195}
196
197pub fn saturation(image: &Image, pass: &Saturation) -> Image {
198 image.map_rgb(|_, _, c| color::saturate(c, pass.amount, pass.weights))
199}
200
201pub fn contrast(image: &Image, pass: &Contrast) -> Image {
202 image.map_rgb(|_, _, c| match *pass {
203 Contrast::Pivot { amount, pivot } => [
204 (c[0] - pivot) * amount + pivot,
205 (c[1] - pivot) * amount + pivot,
206 (c[2] - pivot) * amount + pivot,
207 ],
208 Contrast::Power { power, lift } => [
209 (color::safe_pow(c[0].max(0.0), power) * lift).clamp(0.0, 1.0),
210 (color::safe_pow(c[1].max(0.0), power) * lift).clamp(0.0, 1.0),
211 (color::safe_pow(c[2].max(0.0), power) * lift).clamp(0.0, 1.0),
212 ],
213 })
214}
215
216pub fn warmth(image: &Image, pass: &Warmth) -> Image {
217 image.map_rgb(|_, _, c| match pass.kind {
218 WarmthKind::Linear => {
219 let w = pass.amount;
220 [c[0] * (1.0 + w), c[1], c[2] * (1.0 - w)]
221 }
222 WarmthKind::LumaCurve => {
223 let l = color::luma(c, REC709).clamp(0.0, 1.0);
224 let t = color::smoothstep(pass.shadow, pass.highlight, l);
225 let warm = color::lerp3(
226 [pass.low[0], 1.0, pass.low[1]],
227 [pass.high[0], 1.0, pass.high[1]],
228 t,
229 );
230 let scale = color::lerp3([1.0; 3], warm, pass.amount);
231 [
232 (c[0] * scale[0]).max(0.0),
233 (c[1] * scale[1]).max(0.0),
234 (c[2] * scale[2]).max(0.0),
235 ]
236 }
237 })
238}
239
240pub fn lut(image: &Image, table: &Lut) -> Image {
241 image.map_rgb(|_, _, c| table.sample(c))
242}
243
244pub fn tone(image: &Image, curve: &Tone) -> Image {
245 image.map_rgb(|_, _, c| curve.map(c))
246}
247
248pub fn encode(image: &Image) -> Image {
249 image.map_rgb(|_, _, c| {
250 [
251 color::encode_channel(c[0]),
252 color::encode_channel(c[1]),
253 color::encode_channel(c[2]),
254 ]
255 })
256}
257
258pub fn vignette(image: &Image, pass: &Vignette) -> Image {
259 if pass.strength.abs() < 1e-8 {
260 return image.clone();
261 }
262 let w = image.width.max(1) as f32;
263 let h = image.height.max(1) as f32;
264 image.map_rgb(|x, y, c| {
265 let st_x = (x as f32 + 0.5) / w;
266 let st_y = (y as f32 + 0.5) / h;
267 let factor = match pass.kind {
268 VignetteKind::Power => {
269 let uv_x = st_x * 2.0 - 1.0;
270 let uv_y = st_y * 2.0 - 1.0;
271 let r = (uv_x * uv_x + (uv_y * pass.aspect).powi(2)).sqrt() / pass.scale.max(1e-4);
272 1.0 - pass.strength * color::safe_pow(r.max(0.0), pass.power)
273 }
274 VignetteKind::Smoothstep => {
275 let dx = st_x - 0.5;
276 let dy = (st_y - 0.5) * pass.aspect;
277 let v = (dx * dx + dy * dy).sqrt();
278 1.0 - pass.strength * color::smoothstep(pass.inner, pass.outer, v)
279 }
280 };
281 [c[0] * factor, c[1] * factor, c[2] * factor]
282 })
283}
284
285pub fn grain(image: &Image, pass: &Grain, frame: u32, seed: u32) -> Image {
286 if pass.strength.abs() < 1e-8 && !pass.clamp {
287 return image.clone();
288 }
289 let mixed = hash::mixed(seed, frame);
290 image.map_rgb(|x, y, c| {
291 let n = grain_hash(x, y, mixed ^ 7) + grain_hash(x, y, mixed ^ 19) - 1.0;
292 let l = color::luma(c, REC709);
293 let add = n * pass.strength * (1.0 - pass.response * l);
294 let out = [c[0] + add, c[1] + add, c[2] + add];
295 if pass.clamp {
296 [out[0].max(0.0), out[1].max(0.0), out[2].max(0.0)]
297 } else {
298 out
299 }
300 })
301}
302
303pub fn dither(image: &Image, pass: &Dither, frame: u32, seed: u32) -> Image {
304 if pass.amplitude.abs() < 1e-8 {
305 return image.clone();
306 }
307 image.map_rgb(|x, y, c| {
308 let h = hash::hash21(
309 x as f32 + (seed % 1024) as f32,
310 y as f32 + (frame % 1024) as f32,
311 );
312 let d = (h - 0.5) * pass.amplitude / 255.0;
313 [c[0] + d, c[1] + d, c[2] + d]
314 })
315}
316
317pub fn sample_bilinear(image: &Image, x: f32, y: f32) -> [f32; 4] {
318 image.sample(x, y)
319}
320
321pub fn separable(image: &Image, radius: u32, spread: f32) -> Image {
322 let radius = radius.min(64) as i32;
323 let spread = if spread <= 1e-4 { 1.0 } else { spread };
324 let horizontal = blur_axis(image, radius, spread, true);
325 blur_axis(&horizontal, radius, spread, false)
326}
327
328fn kernel(radius: i32, spread: f32) -> Vec<f32> {
329 let mut weights = Vec::with_capacity((radius * 2 + 1) as usize);
330 let mut total = 0.0;
331 for k in -radius..=radius {
332 let w = (-(k * k) as f32 / spread).exp();
333 weights.push(w);
334 total += w;
335 }
336 if total > 0.0 {
337 for w in &mut weights {
338 *w /= total;
339 }
340 }
341 weights
342}
343
344fn blur_axis(image: &Image, radius: i32, spread: f32, horizontal: bool) -> Image {
345 let weights = kernel(radius, spread);
346 let mut out = image.clone();
347 for y in 0..image.height {
348 for x in 0..image.width {
349 let mut acc = [0.0; 4];
350 for (i, k) in (-radius..=radius).enumerate() {
351 let (sx, sy) = if horizontal {
352 (x as i32 + k, y as i32)
353 } else {
354 (x as i32, y as i32 + k)
355 };
356 let p = image.get(sx, sy);
357 let w = weights[i];
358 for ch in 0..4 {
359 acc[ch] += p[ch] * w;
360 }
361 }
362 out.pixels[image.index(x, y)] = acc;
363 }
364 }
365 out
366}
367
368pub fn keep_rgb(rgb: [f32; 3], threshold: f32, knee: f32, knee_width: f32, soft: f32) -> [f32; 3] {
369 let l = rgb[0].max(rgb[1]).max(rgb[2]);
370 let hard = if l > threshold {
371 (l - threshold).max(0.0) / l.max(1e-4)
372 } else {
373 0.0
374 };
375 let knee_t = if knee_width > 1e-6 {
376 ((l - knee) / knee_width).clamp(0.0, 1.0)
377 } else {
378 0.0
379 };
380 let keep = hard + knee_t * knee_t * soft;
381 [rgb[0] * keep, rgb[1] * keep, rgb[2] * keep]
382}
383
384#[derive(Clone, Copy, Debug, PartialEq, Eq)]
385pub enum Downsample {
386 Box,
387 Tent,
388}
389
390pub fn downsample(image: &Image, factor: u32, filter: Downsample) -> Image {
391 let factor = factor.max(1);
392 let w = (image.width / factor).max(1);
393 let h = (image.height / factor).max(1);
394 Image::from_fn(w, h, |x, y| {
395 let c = if filter == Downsample::Tent {
396 let mut acc = [0.0; 3];
397 let base_x = x as f32 * factor as f32 + factor as f32 * 0.5 - 0.5;
398 let base_y = y as f32 * factor as f32 + factor as f32 * 0.5 - 0.5;
399 for dy in [-1.0, 1.0] {
400 for dx in [-1.0, 1.0] {
401 let s = image.sample(base_x + dx, base_y + dy);
402 acc[0] += s[0];
403 acc[1] += s[1];
404 acc[2] += s[2];
405 }
406 }
407 [acc[0] * 0.25, acc[1] * 0.25, acc[2] * 0.25]
408 } else {
409 let base_x = x as i32 * factor as i32;
410 let base_y = y as i32 * factor as i32;
411 let mut acc = [0.0; 3];
412 let mut n = 0.0;
413 let span = factor.min(64) as i32;
414 for dy in 0..span {
415 for dx in 0..span {
416 let s = image.get(base_x + dx, base_y + dy);
417 acc[0] += s[0];
418 acc[1] += s[1];
419 acc[2] += s[2];
420 n += 1.0;
421 }
422 }
423 [acc[0] / n, acc[1] / n, acc[2] / n]
424 };
425 image::px(c, 1.0)
426 })
427}