Skip to main content

pfx_post/
comic.rs

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}