1use crate::bloom::{self, Bloom};
2use crate::comic::{self, ModernComic};
3use crate::fx::{self, NoiseKind};
4use crate::grade::{self, Contrast, WarmthKind};
5use crate::image::Image;
6use crate::tape::{self, Tape};
7use crate::tone::Tone;
8use crate::wgsl;
9use std::borrow::Cow;
10use std::fmt::Write;
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq)]
13pub struct Buffers {
14 pub depth: bool,
15 pub normal: bool,
16}
17
18impl Buffers {
19 pub const NONE: Self = Self {
20 depth: false,
21 normal: false,
22 };
23
24 pub const GEOMETRY: Self = Self {
25 depth: true,
26 normal: true,
27 };
28}
29
30#[derive(Clone, Copy, Debug, PartialEq, Eq)]
31pub enum Aux {
32 None,
33 Depth,
34 Normal,
35 DepthNormal,
36 Bloom,
37 BloomB,
38 BloomLinear,
39 LinearSampler,
40 Lut,
41 Blue,
42 Grade,
43 GradeBloom,
44 GradeBloomB,
45 GradeBloomLinear,
46}
47
48#[derive(Clone, Debug)]
49pub struct GpuDispatch {
50 pub shader: Cow<'static, str>,
51 pub params: [f32; 16],
52 pub aux: Aux,
53}
54
55#[repr(C)]
56#[derive(Clone, Copy, Debug)]
57pub struct Uniforms {
58 pub size: [u32; 2],
59 pub frame: u32,
60 pub seed: u32,
61 pub params: [f32; 16],
62 pub region: [u32; 4],
63 pub extent: [u32; 4],
64}
65
66impl Uniforms {
67 pub fn new(width: u32, height: u32, frame: u32, seed: u32, params: [f32; 16]) -> Self {
68 Self {
69 size: [width, height],
70 frame,
71 seed,
72 params,
73 region: [0; 4],
74 extent: [width, height, width, height],
75 }
76 }
77
78 pub fn region(mut self, region: [u32; 4]) -> Self {
79 self.region = region;
80 self
81 }
82
83 pub fn extent(mut self, source: (u32, u32), aux: (u32, u32)) -> Self {
84 self.extent = [source.0, source.1, aux.0, aux.1];
85 self
86 }
87
88 pub fn bytes(&self) -> [u8; 112] {
89 let mut out = [0u8; 112];
90 out[0..4].copy_from_slice(&self.size[0].to_ne_bytes());
91 out[4..8].copy_from_slice(&self.size[1].to_ne_bytes());
92 out[8..12].copy_from_slice(&self.frame.to_ne_bytes());
93 out[12..16].copy_from_slice(&self.seed.to_ne_bytes());
94 for (i, value) in self.params.iter().enumerate() {
95 let at = 16 + i * 4;
96 out[at..at + 4].copy_from_slice(&value.to_ne_bytes());
97 }
98 for (i, value) in self.region.iter().enumerate() {
99 let at = 80 + i * 4;
100 out[at..at + 4].copy_from_slice(&value.to_ne_bytes());
101 }
102 for (i, value) in self.extent.iter().enumerate() {
103 let at = 96 + i * 4;
104 out[at..at + 4].copy_from_slice(&value.to_ne_bytes());
105 }
106 out
107 }
108}
109
110pub const BIND_UNIFORM: u32 = 0;
111pub const BIND_SRC: u32 = 1;
112pub const BIND_DST: u32 = 2;
113pub const BIND_AUX: u32 = 3;
114pub const BIND_NORMAL: u32 = 4;
115
116#[derive(Clone, Debug)]
117pub enum Pass {
118 Exposure(f32),
119 Black(f32),
120 Bloom(Bloom),
121 Saturation(grade::Saturation),
122 Contrast(Contrast),
123 Warmth(grade::Warmth),
124 Lut(grade::Lut),
125 Tone(Tone),
126 Vignette(grade::Vignette),
127 Grain(grade::Grain),
128 Dither(grade::Dither),
129 Encode,
130 Cel(fx::Cel),
131 Outline(fx::Outline),
132 Cavity(fx::Cavity),
133 Rim(fx::Rim),
134 Bw(fx::Bw),
135 Noir(fx::Noir),
136 Sepia(f32),
137 Posterize(f32),
138 Neon(fx::Neon),
139 Flicker(f32),
140 Weave(f32),
141 Dust(f32),
142 Scratches(fx::Scratches),
143 Halation(fx::Halation),
144 Aberration(f32),
145 Distortion(f32),
146 Curvature(f32),
147 Scanlines(fx::Scanlines),
148 Aperture(f32),
149 OneBit(fx::OneBit),
150 Halftone(fx::Halftone),
151 Comic(fx::Comic),
152 RubberHose,
153 ModernComic(ModernComic),
154 Watercolor(fx::Watercolor),
155 PaperGrain(fx::PaperGrain),
156 Pixel(fx::Pixelate),
157 Duotone(fx::Duotone),
158 GradientMap(fx::GradientMap),
159 Kuwahara(u32),
160 TiltShift(fx::TiltShift),
161 Tape(Tape),
162 Grade(Vec<Pass>),
163 FusedCavityRim(Vec<Pass>),
164}
165
166#[derive(Clone, Debug)]
167pub struct Chain {
168 pub passes: Vec<Pass>,
169 pub seed: u32,
170 pub frame: u32,
171}
172
173impl Chain {
174 pub fn new() -> Self {
175 Self {
176 passes: Vec::new(),
177 seed: 0,
178 frame: 0,
179 }
180 }
181
182 pub fn is_local(&self) -> bool {
183 self.passes.iter().all(Pass::is_local)
184 }
185
186 pub fn apply(
187 &self,
188 image: &Image,
189 depth: Option<&[f32]>,
190 normal: Option<&[[f32; 3]]>,
191 ) -> Image {
192 let depth = depth.filter(|d| d.len() == image.pixels.len());
193 let normal = normal.filter(|n| n.len() == image.pixels.len());
194 let mut current = image.clone();
195 for pass in &self.passes {
196 current = pass.run(¤t, depth, normal, self.frame, self.seed);
197 }
198 current
199 }
200
201 pub fn split_after_bloom(&self) -> (Self, Self) {
202 let boundary = self
203 .passes
204 .iter()
205 .position(|pass| matches!(pass, Pass::Bloom(_)))
206 .map_or(0, |index| index + 1);
207 (
208 Self {
209 passes: self.passes[..boundary].to_vec(),
210 seed: self.seed,
211 frame: self.frame,
212 },
213 Self {
214 passes: self.passes[boundary..].to_vec(),
215 seed: self.seed,
216 frame: self.frame,
217 },
218 )
219 }
220}
221
222impl Default for Chain {
223 fn default() -> Self {
224 Self::new()
225 }
226}
227
228impl Pass {
229 pub fn name(&self) -> &'static str {
230 match self {
231 Self::Exposure(_) => "exposure",
232 Self::Black(_) => "black",
233 Self::Bloom(_) => "bloom",
234 Self::Saturation(_) => "saturation",
235 Self::Contrast(_) => "contrast",
236 Self::Warmth(_) => "warmth",
237 Self::Lut(_) => "lut",
238 Self::Tone(_) => "tone",
239 Self::Vignette(_) => "vignette",
240 Self::Grain(_) => "grain",
241 Self::Dither(_) => "dither",
242 Self::Encode => "encode",
243 Self::Cel(_) => "cel",
244 Self::Outline(_) => "outline",
245 Self::Cavity(_) => "cavity",
246 Self::Rim(_) => "rim",
247 Self::Bw(_) => "bw",
248 Self::Noir(_) => "noir",
249 Self::Sepia(_) => "sepia",
250 Self::Posterize(_) => "posterize",
251 Self::Neon(_) => "neon",
252 Self::Flicker(_) => "flicker",
253 Self::Weave(_) => "weave",
254 Self::Dust(_) => "dust",
255 Self::Scratches(_) => "scratches",
256 Self::Halation(_) => "halation",
257 Self::Aberration(_) => "aberration",
258 Self::Distortion(_) => "distortion",
259 Self::Curvature(_) => "curvature",
260 Self::Scanlines(_) => "scanlines",
261 Self::Aperture(_) => "aperture",
262 Self::OneBit(_) => "one_bit",
263 Self::Halftone(_) => "halftone",
264 Self::Comic(_) => "comic",
265 Self::RubberHose => "rubber_hose",
266 Self::ModernComic(_) => "modern_comic",
267 Self::Watercolor(_) => "watercolor",
268 Self::PaperGrain(_) => "paper_grain",
269 Self::Pixel(_) => "pixel",
270 Self::Duotone(_) => "duotone",
271 Self::GradientMap(_) => "gradient_map",
272 Self::Kuwahara(_) => "kuwahara",
273 Self::TiltShift(_) => "tilt_shift",
274 Self::Tape(_) => "tape",
275 Self::Grade(_) => "grade",
276 Self::FusedCavityRim(_) => "fused_cavity_rim",
277 }
278 }
279
280 pub fn is_identity(&self) -> bool {
281 match self {
282 Self::Exposure(v) => (*v - 1.0).abs() < 1e-8,
283 Self::Bloom(v) => v.strength.abs() < 1e-8,
284 Self::Vignette(v) => v.strength.abs() < 1e-8,
285 Self::Saturation(v) => (v.amount - 1.0).abs() < 1e-8,
286 Self::Warmth(v) => v.amount.abs() < 1e-8,
287 Self::Halation(v) => v.strength.abs() < 1e-8,
288 _ => false,
289 }
290 }
291
292 pub fn run(
293 &self,
294 image: &Image,
295 depth: Option<&[f32]>,
296 normal: Option<&[[f32; 3]]>,
297 frame: u32,
298 seed: u32,
299 ) -> Image {
300 match self {
301 Self::Exposure(v) => grade::exposure(image, *v),
302 Self::Black(v) => grade::black(image, *v),
303 Self::Bloom(v) => bloom::apply(image, v),
304 Self::Saturation(v) => grade::saturation(image, v),
305 Self::Contrast(v) => grade::contrast(image, v),
306 Self::Warmth(v) => grade::warmth(image, v),
307 Self::Lut(v) => grade::lut(image, v),
308 Self::Tone(v) => grade::tone(image, v),
309 Self::Vignette(v) => grade::vignette(image, v),
310 Self::Grain(v) => grade::grain(image, v, frame, seed),
311 Self::Dither(v) => grade::dither(image, v, frame, seed),
312 Self::Encode => grade::encode(image),
313 Self::Cel(v) => fx::cel(image, v, normal),
314 Self::Outline(v) => fx::outline(image, v, depth, normal),
315 Self::Cavity(v) => fx::cavity(image, v, normal),
316 Self::Rim(v) => fx::rim(image, v, normal),
317 Self::Bw(v) => fx::bw(image, v),
318 Self::Noir(v) => fx::noir(image, v),
319 Self::Sepia(v) => fx::sepia(image, *v),
320 Self::Posterize(v) => fx::posterize(image, *v),
321 Self::Neon(v) => fx::neon(image, v),
322 Self::Flicker(v) => fx::flicker(image, *v, frame, seed),
323 Self::Weave(v) => fx::weave(image, *v, frame, seed),
324 Self::Dust(v) => fx::dust(image, *v, frame, seed),
325 Self::Scratches(v) => fx::scratches(image, v, frame, seed),
326 Self::Halation(v) => fx::halation(image, v),
327 Self::Aberration(v) => fx::aberration(image, *v),
328 Self::Distortion(v) | Self::Curvature(v) => fx::distort(image, *v),
329 Self::Scanlines(v) => fx::scanlines(image, v),
330 Self::Aperture(v) => fx::aperture(image, *v),
331 Self::OneBit(v) => fx::one_bit(image, v),
332 Self::Halftone(v) => fx::halftone(image, v),
333 Self::Comic(v) => fx::comic(image, v, depth, normal),
334 Self::RubberHose => fx::rubber_hose(image, depth, normal, frame, seed),
335 Self::ModernComic(v) => comic::apply(image, v, depth, normal),
336 Self::Watercolor(v) => fx::watercolor(image, v, frame, seed),
337 Self::PaperGrain(v) => fx::paper_grain(image, v, frame, seed),
338 Self::Pixel(v) => fx::pixelate(image, v),
339 Self::Duotone(v) => fx::duotone(image, v),
340 Self::GradientMap(v) => fx::gradient_map(image, v),
341 Self::Kuwahara(v) => fx::kuwahara(image, *v),
342 Self::TiltShift(v) => fx::tilt_shift(image, v, depth),
343 Self::Tape(v) => tape::apply(image, v, frame, seed),
344 Self::Grade(passes) | Self::FusedCavityRim(passes) => {
345 passes.iter().fold(image.clone(), |current, pass| {
346 pass.run(¤t, depth, normal, frame, seed)
347 })
348 }
349 }
350 }
351
352 pub fn region_reach(&self) -> Option<u32> {
353 match self {
354 Self::Bloom(_)
355 | Self::Neon(_)
356 | Self::Halation(_)
357 | Self::Outline(_)
358 | Self::Comic(_)
359 | Self::RubberHose
360 | Self::ModernComic(_)
361 | Self::TiltShift(_)
362 | Self::Tape(_)
363 | Self::Distortion(_)
364 | Self::Curvature(_)
365 | Self::Pixel(_) => None,
366 Self::Grade(passes) => passes
367 .iter()
368 .try_fold(0u32, |reach, pass| Some(reach + pass.region_reach()?)),
369 Self::Kuwahara(radius) => Some((*radius).min(8)),
370 Self::Watercolor(_) => Some(1),
371 Self::Weave(amount) => Some((amount.abs() * 2.0).ceil() as u32 + 1),
372 Self::Aberration(amount) => Some(amount.abs().ceil() as u32 + 1),
373 _ => Some(0),
374 }
375 }
376
377 pub fn is_local(&self) -> bool {
378 match self {
379 Self::Exposure(_)
380 | Self::Black(_)
381 | Self::Saturation(_)
382 | Self::Contrast(_)
383 | Self::Warmth(_)
384 | Self::Lut(_)
385 | Self::Tone(_)
386 | Self::Vignette(_)
387 | Self::Grain(_)
388 | Self::Dither(_)
389 | Self::Encode
390 | Self::Bw(_)
391 | Self::Noir(_)
392 | Self::Sepia(_)
393 | Self::Posterize(_)
394 | Self::Duotone(_) => true,
395 Self::Bloom(bloom) => bloom.strength <= 0.0,
396 Self::Grade(passes) => passes.iter().all(Self::is_local),
397 _ => false,
398 }
399 }
400
401 pub fn is_per_pixel(&self) -> bool {
402 matches!(
403 self,
404 Self::Exposure(_)
405 | Self::Black(_)
406 | Self::Saturation(_)
407 | Self::Contrast(_)
408 | Self::Warmth(_)
409 | Self::Tone(_)
410 | Self::Vignette(_)
411 | Self::Grain(_)
412 | Self::Dither(_)
413 | Self::Encode
414 | Self::Bw(_)
415 | Self::Noir(_)
416 | Self::Sepia(_)
417 | Self::Posterize(_)
418 | Self::Flicker(_)
419 | Self::Scanlines(_)
420 | Self::Aperture(_)
421 | Self::Duotone(_)
422 | Self::GradientMap(_)
423 )
424 }
425
426 pub fn params(&self, buffers: Buffers) -> Vec<[f32; 16]> {
427 match self {
428 Self::Grade(passes) => grade_params(passes),
429 other => other
430 .dispatches(buffers)
431 .into_iter()
432 .map(|dispatch| dispatch.params)
433 .collect(),
434 }
435 }
436
437 pub fn dispatches(&self, buffers: Buffers) -> Vec<GpuDispatch> {
438 match self {
439 Self::Bloom(bloom) => bloom_dispatches(bloom, 1.0),
440 Self::Grade(passes) => grade_dispatches(passes),
441 Self::Halation(pass) => halation_dispatches(pass),
442 Self::Neon(pass) if pass.radius > 0 && pass.strength.abs() >= 1e-8 => vec![
443 GpuDispatch {
444 shader: Cow::Borrowed(wgsl::NEON_EDGE),
445 params: fill(&[(pass.saturation, 0)]),
446 aux: Aux::None,
447 },
448 GpuDispatch {
449 shader: Cow::Borrowed(wgsl::BLOOM_BLUR),
450 params: fill(&[
451 (1.0, 0),
452 (0.0, 1),
453 (pass.radius.min(8).div_ceil(2) as f32, 2),
454 (pass.radius.max(1) as f32 * 1.5, 3),
455 ]),
456 aux: Aux::None,
457 },
458 GpuDispatch {
459 shader: Cow::Borrowed(wgsl::BLOOM_BLUR),
460 params: fill(&[
461 (0.0, 0),
462 (1.0, 1),
463 (pass.radius.min(8).div_ceil(2) as f32, 2),
464 (pass.radius.max(1) as f32 * 1.5, 3),
465 ]),
466 aux: Aux::None,
467 },
468 GpuDispatch {
469 shader: Cow::Borrowed(wgsl::NEON_ADD),
470 params: fill(&[(pass.saturation, 0), (pass.strength, 1)]),
471 aux: Aux::Bloom,
472 },
473 ],
474 other => vec![GpuDispatch {
475 shader: Cow::Borrowed(other.shader()),
476 params: other.pack(buffers),
477 aux: other.aux(),
478 }],
479 }
480 }
481
482 fn shader(&self) -> &'static str {
483 match self {
484 Self::Exposure(_) => wgsl::EXPOSURE,
485 Self::Black(_) => wgsl::BLACK,
486 Self::Bloom(_) => wgsl::BLOOM_ADD,
487 Self::Saturation(_) => wgsl::SATURATION,
488 Self::Contrast(_) => wgsl::CONTRAST,
489 Self::Warmth(_) => wgsl::WARMTH,
490 Self::Lut(_) => wgsl::LUT,
491 Self::Tone(_) => wgsl::TONE,
492 Self::Vignette(_) => wgsl::VIGNETTE,
493 Self::Grain(_) => wgsl::GRAIN,
494 Self::Dither(_) => wgsl::DITHER,
495 Self::Encode => wgsl::ENCODE,
496 Self::Cel(_) => wgsl::CEL,
497 Self::Outline(_) => wgsl::OUTLINE,
498 Self::Cavity(_) => wgsl::CAVITY,
499 Self::Rim(_) => wgsl::RIM,
500 Self::Bw(_) => wgsl::BW,
501 Self::Noir(_) => wgsl::NOIR,
502 Self::Sepia(_) => wgsl::SEPIA,
503 Self::Posterize(_) => wgsl::POSTERIZE,
504 Self::Neon(_) => wgsl::NEON,
505 Self::Flicker(_) => wgsl::FLICKER,
506 Self::Weave(_) => wgsl::WEAVE,
507 Self::Dust(_) => wgsl::DUST,
508 Self::Scratches(_) => wgsl::SCRATCHES,
509 Self::Halation(_) => wgsl::HALATION_ADD,
510 Self::Aberration(_) => wgsl::ABERRATION,
511 Self::Distortion(_) | Self::Curvature(_) => wgsl::DISTORTION,
512 Self::Scanlines(_) => wgsl::SCANLINES,
513 Self::Aperture(_) => wgsl::APERTURE,
514 Self::OneBit(pass) => match pass.kind {
515 NoiseKind::Ordered => wgsl::ONE_BIT,
516 NoiseKind::Blue => wgsl::ONE_BIT_BLUE,
517 },
518 Self::Halftone(_) => wgsl::HALFTONE,
519 Self::Comic(_) => wgsl::COMIC,
520 Self::RubberHose => wgsl::RUBBER_HOSE,
521 Self::ModernComic(_) => wgsl::MODERN_COMIC,
522 Self::Watercolor(_) => wgsl::WATERCOLOR,
523 Self::PaperGrain(_) => wgsl::PAPER_GRAIN,
524 Self::Pixel(_) => wgsl::PIXEL,
525 Self::Duotone(_) => wgsl::DUOTONE,
526 Self::GradientMap(_) => wgsl::GRADIENT,
527 Self::Kuwahara(_) => wgsl::KUWAHARA,
528 Self::TiltShift(_) => wgsl::TILT_SHIFT,
529 Self::Tape(_) => tape::WGSL,
530 Self::Grade(_) => wgsl::EXPOSURE,
531 Self::FusedCavityRim(_) => wgsl::FUSED_CAVITY_RIM,
532 }
533 }
534
535 fn aux(&self) -> Aux {
536 match self {
537 Self::Lut(_) => Aux::Lut,
538 Self::Outline(_) | Self::ModernComic(_) | Self::Comic(_) | Self::RubberHose => {
539 Aux::DepthNormal
540 }
541 Self::Cel(_) | Self::Cavity(_) | Self::Rim(_) | Self::FusedCavityRim(_) => Aux::Normal,
542 Self::TiltShift(_) => Aux::Depth,
543 Self::Tape(_) => Aux::LinearSampler,
544 Self::OneBit(pass) if pass.kind == NoiseKind::Blue => Aux::Blue,
545 _ => Aux::None,
546 }
547 }
548
549 fn pack(&self, buffers: Buffers) -> [f32; 16] {
550 match self {
551 Self::Exposure(v) => fill(&[(*v, 0)]),
552 Self::Black(v) => fill(&[(*v, 0)]),
553 Self::Bloom(v) => bloom_add_params(v),
554 Self::Saturation(v) => fill(&[
555 (v.amount, 0),
556 (v.weights[0], 1),
557 (v.weights[1], 2),
558 (v.weights[2], 3),
559 ]),
560 Self::Contrast(Contrast::Pivot { amount, pivot }) => {
561 fill(&[(*amount, 0), (*pivot, 1), (0.0, 2)])
562 }
563 Self::Contrast(Contrast::Power { power, lift }) => {
564 fill(&[(*power, 0), (*lift, 1), (1.0, 2)])
565 }
566 Self::Warmth(v) => fill(&[
567 (v.amount, 0),
568 (
569 if v.kind == WarmthKind::LumaCurve {
570 1.0
571 } else {
572 0.0
573 },
574 1,
575 ),
576 (v.shadow, 2),
577 (v.highlight, 3),
578 (v.low[0], 4),
579 (v.low[1], 5),
580 (v.high[0], 6),
581 (v.high[1], 7),
582 ]),
583 Self::Lut(v) => fill(&[(v.size as f32, 0)]),
584 Self::Tone(Tone::Aces { gain }) => fill(&[(0.0, 0), (*gain, 1)]),
585 Self::Tone(Tone::Neutral {
586 start,
587 toe,
588 desaturation,
589 clamp,
590 }) => fill(&[
591 (1.0, 0),
592 (*start, 1),
593 (*toe, 2),
594 (*desaturation, 3),
595 (if *clamp { 1.0 } else { 0.0 }, 4),
596 ]),
597 Self::Tone(Tone::Agx) => fill(&[(2.0, 0)]),
598 Self::Vignette(v) => fill(&[
599 (v.strength, 0),
600 (
601 if v.kind == grade::VignetteKind::Smoothstep {
602 1.0
603 } else {
604 0.0
605 },
606 1,
607 ),
608 (v.power, 2),
609 (v.aspect, 3),
610 (v.scale, 4),
611 (v.inner, 5),
612 (v.outer, 6),
613 ]),
614 Self::Grain(v) => fill(&[
615 (v.strength, 0),
616 (v.response, 1),
617 (if v.clamp { 1.0 } else { 0.0 }, 2),
618 ]),
619 Self::Dither(v) => fill(&[(v.amplitude, 0)]),
620 Self::Encode => [0.0; 16],
621 Self::Cel(v) => fill(&[
622 (v.bands as f32, 0),
623 (v.shadow, 1),
624 (v.threshold, 2),
625 (v.softness, 3),
626 (v.spec, 4),
627 (v.spec_roughness, 5),
628 (v.spec_threshold, 6),
629 (if buffers.normal { 1.0 } else { 0.0 }, 7),
630 (v.light[0], 8),
631 (v.light[1], 9),
632 (v.light[2], 10),
633 ]),
634 Self::Outline(v) => fill(&[
635 (v.thickness, 0),
636 (v.alpha, 1),
637 (v.depth_gap, 2),
638 (v.crease_angle.to_radians().cos(), 3),
639 (v.color[0], 4),
640 (v.color[1], 5),
641 (v.color[2], 6),
642 (if v.enabled { 1.0 } else { 0.0 }, 7),
643 (if buffers.depth { 1.0 } else { 0.0 }, 8),
644 (if buffers.normal { 1.0 } else { 0.0 }, 9),
645 (if v.local_color { 1.0 } else { 0.0 }, 10),
646 ]),
647 Self::Cavity(v) => fill(&[
648 (v.strength, 0),
649 (v.distance, 1),
650 (if buffers.normal { 1.0 } else { 0.0 }, 2),
651 ]),
652 Self::Rim(v) => fill(&[
653 (v.strength, 0),
654 (v.width, 1),
655 (v.color[0], 2),
656 (v.color[1], 3),
657 (v.color[2], 4),
658 (if buffers.normal { 1.0 } else { 0.0 }, 5),
659 ]),
660 Self::Bw(v) => fill(&[(v.weights[0], 0), (v.weights[1], 1), (v.weights[2], 2)]),
661 Self::Noir(v) => {
662 let keep = v.keep.unwrap_or([0.0; 3]);
663 fill(&[
664 (v.contrast, 0),
665 (v.crush, 1),
666 (if v.keep.is_some() { 1.0 } else { 0.0 }, 2),
667 (v.keep_range, 3),
668 (keep[0], 4),
669 (keep[1], 5),
670 (keep[2], 6),
671 ])
672 }
673 Self::Sepia(v) => fill(&[(*v, 0)]),
674 Self::Posterize(v) => fill(&[(*v, 0)]),
675 Self::Neon(v) => fill(&[(v.saturation, 0), (v.strength, 1), (v.radius as f32, 2)]),
676 Self::Flicker(v)
677 | Self::Weave(v)
678 | Self::Dust(v)
679 | Self::Aberration(v)
680 | Self::Distortion(v)
681 | Self::Curvature(v)
682 | Self::Aperture(v) => fill(&[(*v, 0)]),
683 Self::Scratches(v) => fill(&[(v.count as f32, 0), (v.strength, 1)]),
684 Self::Halation(v) => fill(&[(v.strength, 0)]),
685 Self::Scanlines(v) => fill(&[(v.strength, 0), (v.period as f32, 1)]),
686 Self::OneBit(_) => [0.0; 16],
687 Self::Halftone(v) => fill(&[
688 (v.cell, 0),
689 (v.angle, 1),
690 (v.ink[0], 2),
691 (v.ink[1], 3),
692 (v.ink[2], 4),
693 ]),
694 Self::Comic(v) => fill(&[
695 (v.levels as f32, 0),
696 (v.ink, 1),
697 (if buffers.depth { 1.0 } else { 0.0 }, 2),
698 (if buffers.normal { 1.0 } else { 0.0 }, 3),
699 ]),
700 Self::RubberHose => fill(&[
701 (if buffers.depth { 1.0 } else { 0.0 }, 0),
702 (if buffers.normal { 1.0 } else { 0.0 }, 1),
703 ]),
704 Self::ModernComic(v) => fill(&[
705 (v.tone_steps as f32, 0),
706 (v.black_threshold, 1),
707 (v.line_weight, 2),
708 (v.interior_line_strength, 3),
709 (v.saturation_lift, 4),
710 (v.highlight_threshold, 5),
711 (if buffers.depth { 1.0 } else { 0.0 }, 6),
712 (if buffers.normal { 1.0 } else { 0.0 }, 7),
713 ]),
714 Self::Watercolor(v) => fill(&[(v.darkening, 0), (v.grain, 1), (v.scale, 2)]),
715 Self::PaperGrain(v) => fill(&[(v.strength, 0), (v.scale, 1)]),
716 Self::Pixel(v) => fill(&[
717 (v.size as f32, 0),
718 (v.levels as f32, 1),
719 (if v.palette { 1.0 } else { 0.0 }, 2),
720 ]),
721 Self::Duotone(v) => fill(&[
722 (v.shadow[0], 0),
723 (v.shadow[1], 1),
724 (v.shadow[2], 2),
725 (v.highlight[0], 4),
726 (v.highlight[1], 5),
727 (v.highlight[2], 6),
728 ]),
729 Self::GradientMap(v) => gradient_params(v),
730 Self::Kuwahara(v) => fill(&[(*v as f32, 0)]),
731 Self::TiltShift(v) => fill(&[
732 (v.focus, 0),
733 (v.range, 1),
734 (v.radius, 2),
735 (if buffers.depth { 1.0 } else { 0.0 }, 3),
736 ]),
737 Self::Tape(v) => v.params(),
738 Self::Grade(passes) => grade_params(passes).pop().unwrap_or([0.0; 16]),
739 Self::FusedCavityRim(passes) => {
740 let [Pass::Cavity(cavity), Pass::Rim(rim)] = passes.as_slice() else {
741 unreachable!()
742 };
743 fill(&[
744 (cavity.strength, 0),
745 (cavity.distance, 1),
746 (rim.strength, 2),
747 (rim.width, 3),
748 (rim.color[0], 4),
749 (rim.color[1], 5),
750 (rim.color[2], 6),
751 (if buffers.normal { 1.0 } else { 0.0 }, 7),
752 ])
753 }
754 }
755 }
756}
757
758fn fill(pairs: &[(f32, usize)]) -> [f32; 16] {
759 let mut out = [0.0; 16];
760 for (value, index) in pairs {
761 out[*index] = *value;
762 }
763 out
764}
765
766fn bloom_add_params(bloom: &Bloom) -> [f32; 16] {
767 fill(&[(bloom.strength, 0)])
768}
769
770fn bloom_dispatches(bloom: &Bloom, scale: f32) -> Vec<GpuDispatch> {
771 match bloom.kind {
772 bloom::BloomKind::Rings => vec![
773 GpuDispatch {
774 shader: Cow::Borrowed(wgsl::BLOOM_RING_DOWN),
775 params: fill(&[
776 (bloom.down.max(1) as f32, 0),
777 (bloom.threshold, 1),
778 (bloom.clamp, 2),
779 (scale, 3),
780 ]),
781 aux: Aux::None,
782 },
783 GpuDispatch {
784 shader: Cow::Borrowed(wgsl::BLOOM_RINGS),
785 params: fill(&[
786 (bloom.unit / bloom.down.max(1) as f32, 3),
787 (bloom.radii[0], 4),
788 (bloom.radii[1], 5),
789 (bloom.radii[2], 6),
790 (bloom.radii[3], 7),
791 (bloom.gains[0], 8),
792 (bloom.gains[1], 9),
793 (bloom.gains[2], 10),
794 (bloom.gains[3], 11),
795 ]),
796 aux: Aux::None,
797 },
798 GpuDispatch {
799 shader: Cow::Borrowed(wgsl::BLOOM_ADD),
800 params: bloom_add_params(bloom),
801 aux: Aux::BloomB,
802 },
803 ],
804 bloom::BloomKind::Box | bloom::BloomKind::Tent => {
805 let tent = if bloom.kind == bloom::BloomKind::Tent {
806 1.0
807 } else {
808 0.0
809 };
810 let box2 = bloom.kind == bloom::BloomKind::Box && bloom.down == 2;
811 let linear = bloom.kind == bloom::BloomKind::Box;
812 let sampled = linear && bloom.linear_taps;
813 let blur = if sampled {
814 Cow::Owned(wgsl::bloom_blur_linear(bloom.radius, bloom.spread))
815 } else {
816 Cow::Borrowed(wgsl::BLOOM_BLUR)
817 };
818 let blur_aux = if sampled {
819 Aux::LinearSampler
820 } else {
821 Aux::None
822 };
823 vec![
824 GpuDispatch {
825 shader: Cow::Borrowed(if box2 {
826 wgsl::BLOOM_DOWN_BOX2
827 } else {
828 wgsl::BLOOM_DOWN
829 }),
830 params: fill(&[
831 (bloom.down.max(1) as f32, 0),
832 (bloom.threshold, 1),
833 (bloom.knee, 2),
834 (bloom.knee_width, 3),
835 (bloom.soft, 4),
836 (tent, 5),
837 (scale, 6),
838 ]),
839 aux: Aux::None,
840 },
841 GpuDispatch {
842 shader: blur.clone(),
843 params: fill(&[
844 (1.0, 0),
845 (0.0, 1),
846 (bloom.radius as f32, 2),
847 (bloom.spread, 3),
848 ]),
849 aux: blur_aux,
850 },
851 GpuDispatch {
852 shader: blur,
853 params: fill(&[
854 (0.0, 0),
855 (1.0, 1),
856 (bloom.radius as f32, 2),
857 (bloom.spread, 3),
858 ]),
859 aux: blur_aux,
860 },
861 GpuDispatch {
862 shader: Cow::Borrowed(if linear {
863 wgsl::BLOOM_ADD_LINEAR
864 } else {
865 wgsl::BLOOM_ADD
866 }),
867 params: bloom_add_params(bloom),
868 aux: if linear { Aux::BloomLinear } else { Aux::Bloom },
869 },
870 ]
871 }
872 }
873}
874
875fn grade_parts(passes: &[Pass]) -> (f32, Option<&Bloom>) {
876 let lead = passes
877 .iter()
878 .take_while(|pass| matches!(pass, Pass::Exposure(_)))
879 .count();
880 let scale = passes[..lead]
881 .iter()
882 .map(|pass| match pass {
883 Pass::Exposure(value) => *value,
884 _ => 1.0,
885 })
886 .product();
887 match passes.get(lead) {
888 Some(Pass::Bloom(bloom)) => (scale, Some(bloom)),
889 _ => (scale, None),
890 }
891}
892
893fn grade_aux(bloom: Option<&Bloom>) -> Aux {
894 match bloom.map(|bloom| bloom.kind) {
895 None => Aux::Grade,
896 Some(bloom::BloomKind::Box) => Aux::GradeBloomLinear,
897 Some(bloom::BloomKind::Tent) => Aux::GradeBloom,
898 Some(bloom::BloomKind::Rings) => Aux::GradeBloomB,
899 }
900}
901
902fn grade_params(passes: &[Pass]) -> Vec<[f32; 16]> {
903 let (scale, bloom) = grade_parts(passes);
904 let mut out: Vec<[f32; 16]> = bloom.map_or_else(Vec::new, |bloom| {
905 let mut stages = bloom_dispatches(bloom, scale);
906 stages.pop();
907 stages.into_iter().map(|stage| stage.params).collect()
908 });
909 out.push(fill(&[
910 (1.0, 0),
911 (bloom.map_or(0.0, |bloom| bloom.strength), 1),
912 ]));
913 out
914}
915
916fn grade_dispatches(passes: &[Pass]) -> Vec<GpuDispatch> {
917 let (scale, bloom) = grade_parts(passes);
918 let aux = grade_aux(bloom);
919 let mut out = bloom.map_or_else(Vec::new, |bloom| {
920 let mut stages = bloom_dispatches(bloom, scale);
921 stages.pop();
922 stages
923 });
924 out.push(GpuDispatch {
925 shader: Cow::Owned(grade_shader(passes)),
926 params: grade_params(passes).pop().unwrap_or([0.0; 16]),
927 aux,
928 });
929 out
930}
931
932pub fn grade_shader(passes: &[Pass]) -> String {
933 let aux = grade_aux(grade_parts(passes).1);
934 let mut out = String::from(wgsl::HEAD);
935 match aux {
936 Aux::GradeBloom | Aux::GradeBloomB => out.push_str(wgsl::BLOOM_TEX),
937 Aux::GradeBloomLinear => out.push_str(wgsl::BLOOM_LINEAR),
938 _ => {}
939 }
940 out.push_str("\n@group(0) @binding(5) var<storage, read> grade: array<vec4<f32>>;\n");
941 out.push_str(wgsl::GRADE_OPS);
942 out.push_str(wgsl::COMMON);
943 out.push_str(wgsl::MAIN);
944 out.push_str(" var c = src;\n");
945 let mut member = 0;
946 for pass in passes {
947 if matches!(pass, Pass::Bloom(_)) {
948 out.push_str(match aux {
949 Aux::GradeBloomLinear => {
950 " let uv = bloom_uv(p);\n c = as_half(vec4<f32>(c.rgb + textureSampleLevel(bloom_tex, bloom_sampler, uv, 0.0).rgb * post.p0.y, c.a));\n"
951 }
952 _ => {
953 " let full = max(vec2<f32>(src_dim()), vec2<f32>(1.0));\n let small = vec2<f32>(bloom_dim());\n let s = sample_bloom((f32(p.x) + 0.5) / full.x * small.x - 0.5, (f32(p.y) + 0.5) / full.y * small.y - 0.5);\n c = as_half(vec4<f32>(c.rgb + s.rgb * post.p0.y, c.a));\n"
954 }
955 });
956 continue;
957 }
958 let at = member * 4;
959 let first = if member == 0 && matches!(pass, Pass::Exposure(_)) {
960 format!("grade[{at}u] * vec4<f32>(post.p0.x, 1.0, 1.0, 1.0)")
961 } else {
962 format!("grade[{at}u]")
963 };
964 let _ = writeln!(
965 out,
966 " c = as_half(grade_{}(c, p, {first}, grade[{}u], grade[{}u], grade[{}u]));",
967 pass.name(),
968 at + 1,
969 at + 2,
970 at + 3
971 );
972 member += 1;
973 }
974 out.push_str(" store(p, c);\n}\n");
975 out
976}
977
978pub fn grade_data(passes: &[Pass]) -> Vec<f32> {
979 let mut out: Vec<f32> = passes
980 .iter()
981 .filter(|pass| !matches!(pass, Pass::Bloom(_)))
982 .flat_map(|member| member.pack(Buffers::NONE))
983 .collect();
984 out.resize(out.len().max(16), 0.0);
985 out
986}
987
988fn halation_dispatches(pass: &fx::Halation) -> Vec<GpuDispatch> {
989 let spread = pass.radius.max(1) as f32 * 0.25;
990 vec![
991 GpuDispatch {
992 shader: Cow::Borrowed(wgsl::HALATION_HOT),
993 params: fill(&[(pass.threshold, 0), (4.0, 1)]),
994 aux: Aux::None,
995 },
996 GpuDispatch {
997 shader: Cow::Borrowed(wgsl::BLOOM_BLUR),
998 params: fill(&[
999 (1.0, 0),
1000 (0.0, 1),
1001 (pass.radius.div_ceil(4) as f32, 2),
1002 (spread, 3),
1003 ]),
1004 aux: Aux::None,
1005 },
1006 GpuDispatch {
1007 shader: Cow::Borrowed(wgsl::BLOOM_BLUR),
1008 params: fill(&[
1009 (0.0, 0),
1010 (1.0, 1),
1011 (pass.radius.div_ceil(4) as f32, 2),
1012 (spread, 3),
1013 ]),
1014 aux: Aux::None,
1015 },
1016 GpuDispatch {
1017 shader: Cow::Borrowed(wgsl::HALATION_ADD),
1018 params: fill(&[(pass.strength, 0)]),
1019 aux: Aux::Bloom,
1020 },
1021 ]
1022}
1023
1024fn gradient_params(pass: &fx::GradientMap) -> [f32; 16] {
1025 let count = pass.count.clamp(2, 4) as usize;
1026 let mut stops = [0.0; 16];
1027 for i in 0..4 {
1028 let stop = pass.stops[i.min(count - 1)];
1029 stops[i * 4] = stop[0];
1030 stops[i * 4 + 1] = stop[1];
1031 stops[i * 4 + 2] = stop[2];
1032 stops[i * 4 + 3] = stop[3];
1033 }
1034 stops
1035}