1use glam::{Vec3, Vec4};
14
15#[derive(Clone, Debug)]
19pub struct ColorGradeParams {
20 pub enabled: bool,
21
22 pub tint: Vec3,
25 pub saturation: f32,
27 pub contrast: f32,
29 pub brightness: f32,
31 pub hue_shift: f32,
33
34 pub lift: Vec3,
37 pub gamma: Vec3,
39 pub gain: Vec3,
41
42 pub shadow_tint: Vec3,
45 pub highlight_tint: Vec3,
47 pub shadow_tint_strength: f32,
49 pub highlight_tint_strength: f32,
51 pub split_midpoint: f32,
53
54 pub curve_strength: f32,
57 pub channel_curves: Vec3,
59
60 pub vignette: f32,
63 pub vignette_feather: f32,
65 pub vignette_roundness: f32,
67 pub vignette_color: Vec3,
69
70 pub lut: Option<ColorLut>,
73 pub lut_strength: f32,
75
76 pub film_look: FilmLook,
78 pub film_look_strength: f32,
80}
81
82impl Default for ColorGradeParams {
83 fn default() -> Self {
84 Self {
85 enabled: true,
86 tint: Vec3::ONE,
87 saturation: 1.0,
88 contrast: 1.0,
89 brightness: 0.0,
90 hue_shift: 0.0,
91 lift: Vec3::ZERO,
92 gamma: Vec3::ONE,
93 gain: Vec3::ONE,
94 shadow_tint: Vec3::ZERO,
95 highlight_tint: Vec3::ZERO,
96 shadow_tint_strength: 0.0,
97 highlight_tint_strength: 0.0,
98 split_midpoint: 0.5,
99 curve_strength: 0.0,
100 channel_curves: Vec3::ZERO,
101 vignette: 0.15,
102 vignette_feather: 0.5,
103 vignette_roundness: 0.8,
104 vignette_color: Vec3::ZERO,
105 lut: None,
106 lut_strength: 1.0,
107 film_look: FilmLook::None,
108 film_look_strength: 1.0,
109 }
110 }
111}
112
113impl ColorGradeParams {
114 pub fn neutral() -> Self { Self::default() }
118
119 pub fn hit_flash(intensity: f32) -> Self {
121 Self {
122 tint: Vec3::new(1.0 + intensity * 0.5, 0.8 - intensity * 0.2, 0.8 - intensity * 0.2),
123 saturation: 1.3,
124 contrast: 1.1,
125 vignette: 0.15 + intensity * 0.4,
126 shadow_tint: Vec3::new(0.4, 0.0, 0.0),
127 shadow_tint_strength: intensity * 0.3,
128 ..Default::default()
129 }
130 }
131
132 pub fn death(progress: f32) -> Self {
134 Self {
135 saturation: 1.0 - progress * 0.85,
136 brightness: -progress * 0.3,
137 contrast: 1.0 + progress * 0.2,
138 vignette: 0.15 + progress * 0.7,
139 tint: Vec3::new(0.8, 0.7, 0.7),
140 lift: Vec3::splat(-progress * 0.05),
141 film_look: FilmLook::Noir,
142 film_look_strength: progress * 0.6,
143 ..Default::default()
144 }
145 }
146
147 pub fn victory() -> Self {
149 Self {
150 tint: Vec3::new(1.15, 1.05, 0.85),
151 saturation: 1.3,
152 brightness: 0.08,
153 highlight_tint: Vec3::new(1.0, 0.9, 0.5),
154 highlight_tint_strength: 0.4,
155 film_look: FilmLook::Golden,
156 film_look_strength: 0.5,
157 ..Default::default()
158 }
159 }
160
161 pub fn danger(severity: f32) -> Self {
163 Self {
164 tint: Vec3::new(0.9 - severity * 0.2, 0.9, 1.1 + severity * 0.1),
165 saturation: 1.0 - severity * 0.3,
166 contrast: 1.0 + severity * 0.15,
167 vignette: 0.2 + severity * 0.5,
168 shadow_tint: Vec3::new(0.0, 0.05, 0.2),
169 shadow_tint_strength: severity * 0.4,
170 ..Default::default()
171 }
172 }
173
174 pub fn retro_crt() -> Self {
176 Self {
177 saturation: 1.4,
178 contrast: 1.2,
179 brightness: -0.05,
180 film_look: FilmLook::RetroTv,
181 film_look_strength: 0.8,
182 vignette: 0.3,
183 vignette_feather: 0.3,
184 tint: Vec3::new(0.95, 1.05, 0.9),
185 ..Default::default()
186 }
187 }
188
189 pub fn dream() -> Self {
191 Self {
192 saturation: 1.2,
193 contrast: 0.85,
194 brightness: 0.1,
195 highlight_tint: Vec3::new(1.0, 0.95, 1.1),
196 highlight_tint_strength: 0.3,
197 shadow_tint: Vec3::new(0.1, 0.1, 0.3),
198 shadow_tint_strength: 0.2,
199 curve_strength: 0.3,
200 film_look: FilmLook::Soft,
201 film_look_strength: 0.6,
202 ..Default::default()
203 }
204 }
205
206 pub fn poison(intensity: f32) -> Self {
208 Self {
209 tint: Vec3::new(0.8, 1.1 + intensity * 0.3, 0.7),
210 saturation: 1.5,
211 contrast: 1.1,
212 shadow_tint: Vec3::new(0.0, 0.3, 0.0),
213 shadow_tint_strength: intensity * 0.4,
214 vignette: 0.2 + intensity * 0.2,
215 vignette_color: Vec3::new(0.0, 0.3, 0.0),
216 ..Default::default()
217 }
218 }
219
220 pub fn lerp(a: &Self, b: &Self, t: f32) -> Self {
222 let t = t.clamp(0.0, 1.0);
223 Self {
224 enabled: a.enabled || b.enabled,
225 tint: Vec3::lerp(a.tint, b.tint, t),
226 saturation: lerp_f32(a.saturation, b.saturation, t),
227 contrast: lerp_f32(a.contrast, b.contrast, t),
228 brightness: lerp_f32(a.brightness, b.brightness, t),
229 hue_shift: lerp_f32(a.hue_shift, b.hue_shift, t),
230 lift: Vec3::lerp(a.lift, b.lift, t),
231 gamma: Vec3::lerp(a.gamma, b.gamma, t),
232 gain: Vec3::lerp(a.gain, b.gain, t),
233 shadow_tint: Vec3::lerp(a.shadow_tint, b.shadow_tint, t),
234 highlight_tint: Vec3::lerp(a.highlight_tint, b.highlight_tint, t),
235 shadow_tint_strength: lerp_f32(a.shadow_tint_strength, b.shadow_tint_strength, t),
236 highlight_tint_strength: lerp_f32(a.highlight_tint_strength, b.highlight_tint_strength, t),
237 split_midpoint: lerp_f32(a.split_midpoint, b.split_midpoint, t),
238 curve_strength: lerp_f32(a.curve_strength, b.curve_strength, t),
239 channel_curves: Vec3::lerp(a.channel_curves, b.channel_curves, t),
240 vignette: lerp_f32(a.vignette, b.vignette, t),
241 vignette_feather: lerp_f32(a.vignette_feather, b.vignette_feather, t),
242 vignette_roundness: lerp_f32(a.vignette_roundness, b.vignette_roundness, t),
243 vignette_color: Vec3::lerp(a.vignette_color, b.vignette_color, t),
244 lut: if t < 0.5 { a.lut.clone() } else { b.lut.clone() },
245 lut_strength: lerp_f32(a.lut_strength, b.lut_strength, t),
246 film_look: if t < 0.5 { a.film_look } else { b.film_look },
247 film_look_strength: lerp_f32(a.film_look_strength, b.film_look_strength, t),
248 }
249 }
250
251 pub fn apply_to_pixel(&self, pixel: Vec3) -> Vec3 {
256 if !self.enabled { return pixel; }
257
258 let mut c = pixel;
259
260 c += Vec3::splat(self.brightness);
262
263 c = (c - 0.5) * self.contrast + 0.5;
265
266 c = c + self.lift;
268 c = Vec3::new(
269 pow_f32(c.x.max(0.0), 1.0 / self.gamma.x.max(0.001)),
270 pow_f32(c.y.max(0.0), 1.0 / self.gamma.y.max(0.001)),
271 pow_f32(c.z.max(0.0), 1.0 / self.gamma.z.max(0.001)),
272 );
273 c *= self.gain;
274
275 c *= self.tint;
277
278 let luma = luminance(c);
280 c = Vec3::splat(luma) + (c - Vec3::splat(luma)) * self.saturation;
281
282 if self.hue_shift.abs() > 0.001 {
284 c = rotate_hue(c, self.hue_shift);
285 }
286
287 if self.curve_strength > 0.001 {
289 c = s_curve_v3(c, self.curve_strength);
290 }
291
292 if self.channel_curves.length_squared() > 0.001 {
294 c.x = apply_channel_curve(c.x, self.channel_curves.x);
295 c.y = apply_channel_curve(c.y, self.channel_curves.y);
296 c.z = apply_channel_curve(c.z, self.channel_curves.z);
297 }
298
299 if self.shadow_tint_strength > 0.001 || self.highlight_tint_strength > 0.001 {
301 let luma2 = luminance(c);
302 let shadow_w = (1.0 - luma2 / self.split_midpoint.max(0.001)).clamp(0.0, 1.0);
303 let highlight_w = (luma2 - self.split_midpoint).max(0.0)
304 / (1.0 - self.split_midpoint).max(0.001);
305 let highlight_w = highlight_w.clamp(0.0, 1.0);
306 c = Vec3::lerp(c, c * (Vec3::ONE + self.shadow_tint),
307 shadow_w * self.shadow_tint_strength);
308 c = Vec3::lerp(c, c * (Vec3::ONE + self.highlight_tint),
309 highlight_w * self.highlight_tint_strength);
310 }
311
312 if let Some(ref lut) = self.lut {
314 let lut_out = lut.sample(c);
315 c = Vec3::lerp(c, lut_out, self.lut_strength);
316 }
317
318 if self.film_look_strength > 0.001 {
320 let film_out = self.film_look.apply(c);
321 c = Vec3::lerp(c, film_out, self.film_look_strength);
322 }
323
324 c.max(Vec3::ZERO)
325 }
326
327 pub fn vignette_at(&self, u: f32, v: f32) -> f32 {
330 if self.vignette < 0.001 { return 1.0; }
331 let dx = (u - 0.5) * 2.0;
332 let dy = (v - 0.5) * 2.0;
333 let r = lerp_f32(
335 dx.abs().max(dy.abs()),
336 (dx * dx + dy * dy).sqrt(),
337 self.vignette_roundness,
338 );
339 let feather = self.vignette_feather.max(0.001);
340 let edge_dist = ((r - (1.0 - self.vignette)) / feather).clamp(0.0, 1.0);
341 1.0 - edge_dist * edge_dist * (3.0 - 2.0 * edge_dist) }
343
344 pub fn apply_full(&self, pixel: Vec3, u: f32, v: f32) -> Vec3 {
346 let graded = self.apply_to_pixel(pixel);
347 let vignette_m = self.vignette_at(u, v);
348 let dark = Vec3::lerp(graded, self.vignette_color, 1.0 - vignette_m);
349 dark
350 }
351
352 pub fn apply_to_image(&self, pixels: &mut [f32], width: usize, height: usize) {
354 let inv_w = 1.0 / width.max(1) as f32;
355 let inv_h = 1.0 / height.max(1) as f32;
356 for y in 0..height {
357 for x in 0..width {
358 let base = (y * width + x) * 3;
359 if base + 2 >= pixels.len() { break; }
360 let c = Vec3::new(pixels[base], pixels[base + 1], pixels[base + 2]);
361 let u = (x as f32 + 0.5) * inv_w;
362 let v = (y as f32 + 0.5) * inv_h;
363 let out = self.apply_full(c, u, v);
364 pixels[base ] = out.x;
365 pixels[base + 1] = out.y;
366 pixels[base + 2] = out.z;
367 }
368 }
369 }
370}
371
372#[derive(Clone, Copy, Debug, PartialEq)]
376pub enum FilmLook {
377 None,
378 Aces,
380 Kodak,
382 Fuji,
384 Noir,
386 Golden,
388 Soft,
390 RetroTv,
392 FadedFilm,
394 TealOrange,
396}
397
398impl FilmLook {
399 pub fn apply(self, c: Vec3) -> Vec3 {
400 match self {
401 FilmLook::None => c,
402 FilmLook::Aces => aces_filmic(c),
403 FilmLook::Kodak => kodak_look(c),
404 FilmLook::Fuji => fuji_look(c),
405 FilmLook::Noir => noir_look(c),
406 FilmLook::Golden => golden_look(c),
407 FilmLook::Soft => soft_look(c),
408 FilmLook::RetroTv => retro_tv_look(c),
409 FilmLook::FadedFilm => faded_film_look(c),
410 FilmLook::TealOrange => teal_orange_look(c),
411 }
412 }
413
414 pub fn name(self) -> &'static str {
415 match self {
416 FilmLook::None => "None",
417 FilmLook::Aces => "ACES",
418 FilmLook::Kodak => "Kodak",
419 FilmLook::Fuji => "Fuji",
420 FilmLook::Noir => "Noir",
421 FilmLook::Golden => "Golden",
422 FilmLook::Soft => "Soft",
423 FilmLook::RetroTv => "Retro TV",
424 FilmLook::FadedFilm => "Faded Film",
425 FilmLook::TealOrange => "Teal & Orange",
426 }
427 }
428}
429
430fn aces_filmic(c: Vec3) -> Vec3 {
432 let a = 2.51_f32;
433 let b = 0.03_f32;
434 let cc = 2.43_f32;
435 let d = 0.59_f32;
436 let e = 0.14_f32;
437 let x = c;
438 ((x * (a * x + b)) / (x * (cc * x + d) + e)).clamp(Vec3::ZERO, Vec3::ONE)
439}
440
441fn kodak_look(c: Vec3) -> Vec3 {
442 let lifted = c * 0.93 + Vec3::new(0.02, 0.015, 0.01);
444 let warm = lifted * Vec3::new(1.08, 1.02, 0.95);
445 soft_knee_compress(warm, 0.85, 0.1)
446}
447
448fn fuji_look(c: Vec3) -> Vec3 {
449 let cooler = c * Vec3::new(0.97, 1.0, 1.06);
450 let slight_desat = {
451 let luma = luminance(cooler);
452 Vec3::lerp(Vec3::splat(luma), cooler, 0.88)
453 };
454 soft_knee_compress(slight_desat, 0.9, 0.08)
455}
456
457fn noir_look(c: Vec3) -> Vec3 {
458 let luma = luminance(c);
459 let grey = Vec3::splat(luma);
461 let contrasted = s_curve_v3(grey, 0.7);
462 contrasted * Vec3::new(0.95, 0.92, 0.9) }
464
465fn golden_look(c: Vec3) -> Vec3 {
466 let warm = c * Vec3::new(1.15, 1.05, 0.82);
467 soft_knee_compress(warm, 0.88, 0.1)
469}
470
471fn soft_look(c: Vec3) -> Vec3 {
472 let soft_c = (c - 0.5) * 0.8 + 0.5;
474 let bloom = (c - Vec3::splat(0.7)).max(Vec3::ZERO) * 0.3;
475 soft_c + bloom
476}
477
478fn retro_tv_look(c: Vec3) -> Vec3 {
479 let r = c.x * 0.9 + c.y * 0.1;
481 let g = c.y;
482 let b = c.z * 0.85;
483 let bleed = Vec3::new(r, g, b);
485 let luma = luminance(bleed);
486 Vec3::lerp(Vec3::splat(luma), bleed, 1.4)
487}
488
489fn faded_film_look(c: Vec3) -> Vec3 {
490 c * 0.85 + Vec3::splat(0.06)
492}
493
494fn teal_orange_look(c: Vec3) -> Vec3 {
495 let luma = luminance(c);
496 let shadow_w = (1.0 - luma).clamp(0.0, 1.0).powi(2);
497 let highlight_w = luma.clamp(0.0, 1.0).powi(2);
498 let teal_shadows = Vec3::lerp(c, Vec3::new(0.3, 0.7, 0.8), shadow_w * 0.35);
500 Vec3::lerp(teal_shadows, Vec3::new(1.1, 0.7, 0.4), highlight_w * 0.35)
502}
503
504#[derive(Clone, Debug)]
508pub struct ColorLut {
509 pub size: usize,
510 pub entries: Vec<Vec3>,
512}
513
514impl ColorLut {
515 pub fn identity(size: usize) -> Self {
517 let n = size * size * size;
518 let mut entries = Vec::with_capacity(n);
519 let inv = (size - 1) as f32;
520 for b in 0..size {
521 for g in 0..size {
522 for r in 0..size {
523 entries.push(Vec3::new(r as f32 / inv, g as f32 / inv, b as f32 / inv));
524 }
525 }
526 }
527 Self { size, entries }
528 }
529
530 pub fn from_grade(grade: &ColorGradeParams, size: usize) -> Self {
532 let mut lut = Self::identity(size);
533 for e in &mut lut.entries {
534 *e = grade.apply_to_pixel(*e);
535 }
536 lut
537 }
538
539 pub fn sample(&self, color: Vec3) -> Vec3 {
541 let n = self.size;
542 let s = (n - 1) as f32;
543 let cr = (color.x * s).clamp(0.0, s);
544 let cg = (color.y * s).clamp(0.0, s);
545 let cb = (color.z * s).clamp(0.0, s);
546 let r0 = cr.floor() as usize;
547 let g0 = cg.floor() as usize;
548 let b0 = cb.floor() as usize;
549 let r1 = (r0 + 1).min(n - 1);
550 let g1 = (g0 + 1).min(n - 1);
551 let b1 = (b0 + 1).min(n - 1);
552 let tr = cr.fract();
553 let tg = cg.fract();
554 let tb = cb.fract();
555
556 let idx = |r: usize, g: usize, b: usize| b * n * n + g * n + r;
557
558 let c000 = self.entries[idx(r0, g0, b0)];
559 let c100 = self.entries[idx(r1, g0, b0)];
560 let c010 = self.entries[idx(r0, g1, b0)];
561 let c110 = self.entries[idx(r1, g1, b0)];
562 let c001 = self.entries[idx(r0, g0, b1)];
563 let c101 = self.entries[idx(r1, g0, b1)];
564 let c011 = self.entries[idx(r0, g1, b1)];
565 let c111 = self.entries[idx(r1, g1, b1)];
566
567 let c00 = Vec3::lerp(c000, c100, tr);
568 let c01 = Vec3::lerp(c001, c101, tr);
569 let c10 = Vec3::lerp(c010, c110, tr);
570 let c11 = Vec3::lerp(c011, c111, tr);
571 let c0 = Vec3::lerp(c00, c10, tg);
572 let c1 = Vec3::lerp(c01, c11, tg);
573 Vec3::lerp(c0, c1, tb)
574 }
575
576 pub fn to_cube_string(&self, title: &str) -> String {
578 let mut out = format!("TITLE \"{}\"\nLUT_3D_SIZE {}\n", title, self.size);
579 for e in &self.entries {
580 out.push_str(&format!("{:.6} {:.6} {:.6}\n", e.x, e.y, e.z));
581 }
582 out
583 }
584}
585
586#[derive(Clone, Debug)]
590pub struct ColorGradeKeyframe {
591 pub time: f32,
592 pub grade: ColorGradeParams,
593 pub easing: u8,
595}
596
597#[derive(Clone, Debug)]
599pub struct AnimatedColorGrade {
600 pub keyframes: Vec<ColorGradeKeyframe>,
601}
602
603impl AnimatedColorGrade {
604 pub fn new() -> Self { Self { keyframes: Vec::new() } }
605
606 pub fn add(mut self, time: f32, grade: ColorGradeParams) -> Self {
607 self.keyframes.push(ColorGradeKeyframe { time, grade, easing: 1 });
608 self.keyframes.sort_by(|a, b| a.time.partial_cmp(&b.time).unwrap());
609 self
610 }
611
612 pub fn add_eased(mut self, time: f32, grade: ColorGradeParams, easing: u8) -> Self {
613 self.keyframes.push(ColorGradeKeyframe { time, grade, easing });
614 self.keyframes.sort_by(|a, b| a.time.partial_cmp(&b.time).unwrap());
615 self
616 }
617
618 pub fn evaluate(&self, t: f32) -> ColorGradeParams {
620 if self.keyframes.is_empty() { return ColorGradeParams::default(); }
621 if self.keyframes.len() == 1 { return self.keyframes[0].grade.clone(); }
622 if t <= self.keyframes[0].time { return self.keyframes[0].grade.clone(); }
623 let last = self.keyframes.last().unwrap();
624 if t >= last.time { return last.grade.clone(); }
625
626 let i = self.keyframes.partition_point(|k| k.time <= t) - 1;
628 let k0 = &self.keyframes[i];
629 let k1 = &self.keyframes[i + 1];
630 let span = k1.time - k0.time;
631 let raw_t = if span < 1e-6 { 0.0 } else { (t - k0.time) / span };
632 let et = ease(raw_t, k0.easing);
633 ColorGradeParams::lerp(&k0.grade, &k1.grade, et)
634 }
635}
636
637fn ease(t: f32, mode: u8) -> f32 {
638 match mode {
639 0 => t,
640 1 => t * t * (3.0 - 2.0 * t), 2 => t * t, 3 => t * (2.0 - t), _ => t,
644 }
645}
646
647#[inline]
650fn luminance(c: Vec3) -> f32 {
651 c.x * 0.2126 + c.y * 0.7152 + c.z * 0.0722
652}
653
654#[inline]
655fn lerp_f32(a: f32, b: f32, t: f32) -> f32 { a + (b - a) * t }
656
657#[inline]
658fn pow_f32(base: f32, exp: f32) -> f32 {
659 if base <= 0.0 { 0.0 } else { base.powf(exp) }
660}
661
662fn rotate_hue(c: Vec3, degrees: f32) -> Vec3 {
663 let angle = degrees.to_radians();
664 let cos_a = angle.cos();
665 let sin_a = angle.sin();
666 let sqrt3 = 3.0_f32.sqrt();
668 let r = c.x * (cos_a + (1.0 - cos_a) / 3.0) + c.y * ((1.0 - cos_a) / 3.0 - sin_a / sqrt3)
669 + c.z * ((1.0 - cos_a) / 3.0 + sin_a / sqrt3);
670 let g = c.x * ((1.0 - cos_a) / 3.0 + sin_a / sqrt3) + c.y * (cos_a + (1.0 - cos_a) / 3.0)
671 + c.z * ((1.0 - cos_a) / 3.0 - sin_a / sqrt3);
672 let b = c.x * ((1.0 - cos_a) / 3.0 - sin_a / sqrt3) + c.y * ((1.0 - cos_a) / 3.0 + sin_a / sqrt3)
673 + c.z * (cos_a + (1.0 - cos_a) / 3.0);
674 Vec3::new(r, g, b).max(Vec3::ZERO)
675}
676
677fn s_curve_v3(c: Vec3, strength: f32) -> Vec3 {
678 Vec3::new(
679 s_curve(c.x, strength),
680 s_curve(c.y, strength),
681 s_curve(c.z, strength),
682 )
683}
684
685fn s_curve(x: f32, strength: f32) -> f32 {
686 let x = x.clamp(0.0, 1.0);
687 let curved = x * x * (3.0 - 2.0 * x); lerp_f32(x, curved, strength)
689}
690
691fn apply_channel_curve(x: f32, strength: f32) -> f32 {
692 s_curve(x, strength)
694}
695
696fn soft_knee_compress(c: Vec3, knee: f32, width: f32) -> Vec3 {
697 Vec3::new(
698 soft_knee_channel(c.x, knee, width),
699 soft_knee_channel(c.y, knee, width),
700 soft_knee_channel(c.z, knee, width),
701 )
702}
703
704fn soft_knee_channel(x: f32, knee: f32, width: f32) -> f32 {
705 if x <= knee - width {
706 x
707 } else if x >= knee + width {
708 knee + (x - knee) * 0.1 } else {
710 let t = (x - (knee - width)) / (2.0 * width);
711 let blend = t * t * (3.0 - 2.0 * t);
712 let compressed = knee + (x - knee) * 0.1;
713 lerp_f32(x, compressed, blend)
714 }
715}
716
717pub const COLOR_GRADE_FRAG: &str = r#"
721#version 330 core
722in vec2 vUv;
723out vec4 fColor;
724
725uniform sampler2D uScene;
726uniform float uSaturation;
727uniform float uContrast;
728uniform float uBrightness;
729uniform float uHueShift;
730uniform vec3 uTint;
731uniform vec3 uLift;
732uniform vec3 uGamma;
733uniform vec3 uGain;
734uniform float uVignette;
735uniform float uVignetteFeather;
736uniform float uVignetteRoundness;
737
738float luminance(vec3 c) { return dot(c, vec3(0.2126, 0.7152, 0.0722)); }
739
740vec3 saturation(vec3 c, float s) {
741 float luma = luminance(c);
742 return mix(vec3(luma), c, s);
743}
744
745vec3 adjustContrast(vec3 c, float contrast) {
746 return (c - 0.5) * contrast + 0.5;
747}
748
749void main() {
750 vec3 c = texture(uScene, vUv).rgb;
751
752 // Brightness / Contrast
753 c += uBrightness;
754 c = adjustContrast(c, uContrast);
755
756 // Lift / Gamma / Gain
757 c = c + uLift;
758 c = pow(max(c, 0.0), 1.0 / max(uGamma, vec3(0.001)));
759 c *= uGain;
760
761 // Tint + Saturation
762 c *= uTint;
763 c = saturation(c, uSaturation);
764
765 // Vignette
766 vec2 uv = vUv * 2.0 - 1.0;
767 float r = mix(max(abs(uv.x), abs(uv.y)), length(uv), uVignetteRoundness);
768 float vig = 1.0 - smoothstep(1.0 - uVignette, 1.0 - uVignette + uVignetteFeather, r);
769 c *= vig;
770
771 fColor = vec4(clamp(c, 0.0, 1.0), 1.0);
772}
773"#;
774
775#[cfg(test)]
778mod tests {
779 use super::*;
780
781 #[test]
782 fn test_identity_grade() {
783 let grade = ColorGradeParams::neutral();
784 let pixel = Vec3::new(0.5, 0.3, 0.7);
785 let out = grade.apply_to_pixel(pixel);
786 assert!((out.x - pixel.x).abs() < 0.05);
788 assert!((out.y - pixel.y).abs() < 0.05);
789 assert!((out.z - pixel.z).abs() < 0.05);
790 }
791
792 #[test]
793 fn test_greyscale() {
794 let mut grade = ColorGradeParams::neutral();
795 grade.saturation = 0.0;
796 let pixel = Vec3::new(1.0, 0.0, 0.0);
797 let out = grade.apply_to_pixel(pixel);
798 assert!((out.x - out.y).abs() < 0.01);
800 assert!((out.y - out.z).abs() < 0.01);
801 }
802
803 #[test]
804 fn test_vignette_center() {
805 let grade = ColorGradeParams::default();
806 let v = grade.vignette_at(0.5, 0.5);
807 assert!(v > 0.99); }
809
810 #[test]
811 fn test_vignette_corner() {
812 let grade = ColorGradeParams::default();
813 let v = grade.vignette_at(0.0, 0.0);
814 assert!(v < 0.9); }
816
817 #[test]
818 fn test_lerp() {
819 let a = ColorGradeParams::neutral();
820 let b = ColorGradeParams::death(1.0);
821 let mid = ColorGradeParams::lerp(&a, &b, 0.5);
822 assert!((mid.saturation - 0.5 * (1.0 + (1.0 - 0.85))).abs() < 0.1);
823 }
824
825 #[test]
826 fn test_lut_identity() {
827 let lut = ColorLut::identity(17);
828 let c = Vec3::new(0.4, 0.6, 0.8);
829 let out = lut.sample(c);
830 assert!((out.x - c.x).abs() < 0.01);
831 assert!((out.y - c.y).abs() < 0.01);
832 assert!((out.z - c.z).abs() < 0.01);
833 }
834
835 #[test]
836 fn test_lut_from_grade() {
837 let grade = ColorGradeParams::death(0.5);
838 let lut = ColorLut::from_grade(&grade, 9);
839 assert_eq!(lut.entries.len(), 9 * 9 * 9);
840 }
841
842 #[test]
843 fn test_film_looks_dont_panic() {
844 let pixel = Vec3::new(0.3, 0.5, 0.7);
845 for look in [FilmLook::Aces, FilmLook::Kodak, FilmLook::Fuji, FilmLook::Noir,
846 FilmLook::Golden, FilmLook::Soft, FilmLook::RetroTv,
847 FilmLook::FadedFilm, FilmLook::TealOrange] {
848 let out = look.apply(pixel);
849 assert!(out.x.is_finite());
850 assert!(out.y.is_finite());
851 assert!(out.z.is_finite());
852 }
853 }
854
855 #[test]
856 fn test_animated_grade() {
857 let anim = AnimatedColorGrade::new()
858 .add(0.0, ColorGradeParams::neutral())
859 .add(1.0, ColorGradeParams::death(1.0));
860 let mid = anim.evaluate(0.5);
861 assert!((mid.saturation - 0.5 * (1.0 + 0.15)).abs() < 0.3);
862 assert_eq!(anim.evaluate(0.0).brightness, 0.0);
863 }
864
865 #[test]
866 fn test_image_processing() {
867 let mut pixels = vec![0.5_f32, 0.5, 0.5, 0.3, 0.6, 0.9];
868 let grade = ColorGradeParams::neutral();
869 grade.apply_to_image(&mut pixels, 2, 1);
870 for p in &pixels { assert!(p.is_finite()); }
872 }
873
874 #[test]
875 fn test_cube_export() {
876 let lut = ColorLut::identity(3);
877 let cube = lut.to_cube_string("Test LUT");
878 assert!(cube.contains("LUT_3D_SIZE 3"));
879 assert!(cube.contains("TITLE"));
880 }
881}