1use crate::color::{self, REC709};
2use crate::image::Image;
3
4#[derive(Clone, Copy, Debug)]
5pub struct ModernComic {
6 pub tone_steps: u32,
7 pub black_threshold: f32,
8 pub line_weight: f32,
9 pub interior_line_strength: f32,
10 pub saturation_lift: f32,
11 pub highlight_threshold: f32,
12}
13
14impl Default for ModernComic {
15 fn default() -> Self {
16 Self {
17 tone_steps: 4,
18 black_threshold: 0.035,
19 line_weight: 1.0,
20 interior_line_strength: 0.78,
21 saturation_lift: 1.12,
22 highlight_threshold: 0.72,
23 }
24 }
25}
26
27fn luma_at(image: &Image, x: i32, y: i32) -> f32 {
28 let p = image.get(x, y);
29 color::dot3([p[0], p[1], p[2]], REC709).clamp(0.0, 1.0)
30}
31
32fn depth_at(image: &Image, depth: &[f32], x: i32, y: i32) -> f32 {
33 let x = x.clamp(0, image.width as i32 - 1) as u32;
34 let y = y.clamp(0, image.height as i32 - 1) as u32;
35 depth[image.index(x, y)]
36}
37
38fn normal_at(image: &Image, normal: &[[f32; 3]], x: i32, y: i32) -> [f32; 3] {
39 let x = x.clamp(0, image.width as i32 - 1) as u32;
40 let y = y.clamp(0, image.height as i32 - 1) as u32;
41 color::normalize(normal[image.index(x, y)])
42}
43
44pub fn apply(
45 image: &Image,
46 pass: &ModernComic,
47 depth: Option<&[f32]>,
48 normal: Option<&[[f32; 3]]>,
49) -> Image {
50 if image.width == 0 || image.height == 0 {
51 return image.clone();
52 }
53 let depth = depth.filter(|v| v.len() == image.pixels.len());
54 let normal = normal.filter(|v| v.len() == image.pixels.len());
55 let steps = pass.tone_steps.clamp(3, 5) as f32;
56 image.map_rgb(|x, y, c| {
57 let x = x as i32;
58 let y = y as i32;
59 let left_color = image.get(x - 1, y);
60 let right_color = image.get(x + 1, y);
61 let up_color = image.get(x, y - 1);
62 let down_color = image.get(x, y + 1);
63 let smooth = std::array::from_fn(|channel| {
64 (c[channel] * 4.0
65 + left_color[channel]
66 + right_color[channel]
67 + up_color[channel]
68 + down_color[channel])
69 * 0.125
70 });
71 let light = color::dot3(smooth, REC709).clamp(0.0, 1.0);
72 let left = color::dot3([left_color[0], left_color[1], left_color[2]], REC709);
73 let right = color::dot3([right_color[0], right_color[1], right_color[2]], REC709);
74 let up = color::dot3([up_color[0], up_color[1], up_color[2]], REC709);
75 let down = color::dot3([down_color[0], down_color[1], down_color[2]], REC709);
76 let gradient = (left - right).abs().max((up - down).abs()) * 0.5;
77 let scaled = light * steps;
78 let base = scaled.floor().min(steps - 1.0);
79 let aa = (gradient * steps * 0.5).clamp(0.002, 0.18);
80 let blend = color::smoothstep(1.0 - aa, 1.0, scaled - base);
81 let low = (base + 0.5) / steps;
82 let high = if base >= steps - 2.0 {
83 1.0
84 } else {
85 (base + 1.5) / steps
86 };
87 let tone =
88 if base >= steps - 1.0 || (light >= pass.highlight_threshold && base >= steps - 2.0) {
89 1.0
90 } else {
91 color::lerp(low, high, blend)
92 };
93 let scale = tone / light.max(0.025);
94 let black_aa = (gradient * 0.5).max(0.001);
95 let black_gate = color::smoothstep(
96 pass.black_threshold - black_aa,
97 pass.black_threshold + black_aa,
98 light,
99 );
100 let mut out = color::saturate(
101 smooth.map(|v| (v * scale).clamp(0.0, 1.0)),
102 pass.saturation_lift,
103 REC709,
104 )
105 .map(|v| (v * black_gate).clamp(0.0, 1.0));
106 let surround = (luma_at(image, x - 2, y)
107 + luma_at(image, x + 2, y)
108 + luma_at(image, x, y - 2)
109 + luma_at(image, x, y + 2))
110 * 0.25;
111 let detail = (surround - color::dot3(c, REC709)).max(gradient * 0.35);
112 let feature =
113 color::smoothstep(0.12, 0.26, detail) * pass.interior_line_strength.clamp(0.0, 1.0);
114 let mut silhouette = 0.0_f32;
115 let mut crease = 0.0_f32;
116 let distance = if let Some(depth) = depth {
117 let z = depth_at(image, depth, x, y);
118 if z < 0.55 { 3 } else { 2 }
119 } else {
120 1
121 };
122 for (dx, dy) in [(distance, 0), (-distance, 0), (0, distance), (0, -distance)] {
123 if let Some(depth) = depth {
124 let neighbor = image.get(x + dx, y + dy);
125 let color_gap = (c[0] - neighbor[0])
126 .abs()
127 .max((c[1] - neighbor[1]).abs())
128 .max((c[2] - neighbor[2]).abs());
129 silhouette = silhouette.max(color::smoothstep(0.10, 0.28, color_gap));
130 let gap =
131 (depth_at(image, depth, x, y) - depth_at(image, depth, x + dx, y + dy)).abs();
132 silhouette = silhouette.max(color::smoothstep(0.002, 0.015, gap));
133 }
134 if let Some(normal) = normal {
135 let a = normal_at(image, normal, x, y);
136 let b = normal_at(image, normal, x + dx.signum(), y + dy.signum());
137 crease = crease.max(color::smoothstep(0.16, 0.48, 1.0 - color::dot3(a, b)));
138 }
139 }
140 let ink = (silhouette * pass.line_weight.clamp(0.0, 1.0))
141 .max(crease * pass.line_weight.clamp(0.0, 1.0) * 0.78)
142 .max(feature);
143 let dark_ink = [0.005, 0.004, 0.008];
144 out = color::lerp3(out, dark_ink, ink.clamp(0.0, 1.0));
145 out
146 })
147}