Skip to main content

pfx_post/
lines.rs

1#[derive(Clone, Copy, Debug, PartialEq)]
2pub struct Lines {
3    pub enabled: bool,
4    pub thickness: f32,
5    pub color: [f32; 3],
6    pub alpha: f32,
7    pub crease_angle: f32,
8    pub silhouette: bool,
9    pub border: bool,
10    pub crease: bool,
11    pub contour: bool,
12    pub external_contour: bool,
13}
14
15impl Default for Lines {
16    fn default() -> Self {
17        Self {
18            enabled: false,
19            thickness: 2.0,
20            color: [
21                crate::view::srgb_decode(0x17 as f32 / 255.0),
22                crate::view::srgb_decode(0x1a as f32 / 255.0),
23                crate::view::srgb_decode(0x34 as f32 / 255.0),
24            ],
25            alpha: 1.0,
26            crease_angle: 134.0,
27            silhouette: true,
28            border: true,
29            crease: true,
30            contour: false,
31            external_contour: true,
32        }
33    }
34}
35
36#[derive(Clone, Copy, Debug, PartialEq, Eq)]
37pub enum Edge {
38    External,
39    Silhouette,
40    Contour,
41    Border,
42    Crease,
43}
44
45pub struct Gbuffer<'a> {
46    pub width: u32,
47    pub height: u32,
48    pub depth: &'a [f32],
49    pub id: &'a [u32],
50    pub normal: &'a [[f32; 3]],
51}
52
53impl Gbuffer<'_> {
54    fn valid(&self) -> bool {
55        let n = self.width as usize * self.height as usize;
56        self.depth.len() == n && self.id.len() == n && self.normal.len() == n
57    }
58
59    fn at(&self, x: i64, y: i64) -> Option<usize> {
60        (x >= 0 && y >= 0 && x < i64::from(self.width) && y < i64::from(self.height))
61            .then(|| y as usize * self.width as usize + x as usize)
62    }
63
64    pub fn classify(
65        &self,
66        a: usize,
67        b: usize,
68        before: Option<usize>,
69        after: Option<usize>,
70        crease_angle: f32,
71    ) -> Option<Edge> {
72        let (ia, ib) = (self.id[a], self.id[b]);
73        match (ia != 0, ib != 0) {
74            (false, false) => return None,
75            (true, false) | (false, true) => return Some(Edge::External),
76            _ => {}
77        }
78        let (za, zb) = (self.depth[a], self.depth[b]);
79        let slope = |p: Option<usize>, q: usize| {
80            p.filter(|&p| self.id[p] == self.id[q])
81                .map_or(0.0, |p| (self.depth[p] - self.depth[q]).abs())
82        };
83        let local = slope(before, a).max(slope(after, b));
84        let jump = (zb - za).abs();
85        if jump > 2.0 * local + 0.002 * za.max(zb) {
86            return Some(if ia != ib {
87                Edge::Silhouette
88            } else {
89                Edge::Contour
90            });
91        }
92        if ia != ib {
93            return Some(Edge::Border);
94        }
95        let (na, nb) = (self.normal[a], self.normal[b]);
96        let cosine = na[0] * nb[0] + na[1] * nb[1] + na[2] * nb[2];
97        let limit = (180.0 - crease_angle).to_radians().cos();
98        (cosine < limit).then_some(Edge::Crease)
99    }
100}
101
102impl Lines {
103    pub fn width_px(&self, image_width: u32) -> f32 {
104        self.thickness * image_width as f32 / 1024.0
105    }
106
107    pub fn draws(&self, edge: Edge) -> bool {
108        match edge {
109            Edge::External => self.external_contour || self.silhouette,
110            Edge::Silhouette => self.silhouette,
111            Edge::Contour => self.contour,
112            Edge::Border => self.border || self.crease,
113            Edge::Crease => self.crease,
114        }
115    }
116
117    pub fn segments(&self, g: &Gbuffer) -> Result<Vec<[f32; 4]>, String> {
118        if !g.valid() {
119            return Err("the outline's depth, id and normal do not match its size".into());
120        }
121        let mut out = Vec::new();
122        for y in 0..i64::from(g.height) {
123            for x in 0..i64::from(g.width) {
124                let a = g.at(x, y).unwrap();
125                if let Some(b) = g.at(x + 1, y)
126                    && let Some(edge) =
127                        g.classify(a, b, g.at(x - 1, y), g.at(x + 2, y), self.crease_angle)
128                    && self.draws(edge)
129                {
130                    let edge_x = x as f32 + 1.0;
131                    out.push([edge_x, y as f32, edge_x, y as f32 + 1.0]);
132                }
133                if let Some(b) = g.at(x, y + 1)
134                    && let Some(edge) =
135                        g.classify(a, b, g.at(x, y - 1), g.at(x, y + 2), self.crease_angle)
136                    && self.draws(edge)
137                {
138                    let edge_y = y as f32 + 1.0;
139                    out.push([x as f32, edge_y, x as f32 + 1.0, edge_y]);
140                }
141            }
142        }
143        Ok(out)
144    }
145
146    pub fn coverage(&self, g: &Gbuffer, image_width: u32) -> Result<Vec<f32>, String> {
147        let segments = self.segments(g)?;
148        let half = self.width_px(image_width) * 0.5;
149        let reach = (half + 1.0).ceil() as i64;
150        let (w, h) = (i64::from(g.width), i64::from(g.height));
151        let mut coverage = vec![0.0f32; (w * h) as usize];
152        for [x0, y0, x1, y1] in segments {
153            let (cx, cy) = (
154                ((x0 + x1) * 0.5).floor() as i64,
155                ((y0 + y1) * 0.5).floor() as i64,
156            );
157            for py in (cy - reach).max(0)..(cy + reach + 1).min(h) {
158                for px in (cx - reach).max(0)..(cx + reach + 1).min(w) {
159                    let p = [px as f32 + 0.5, py as f32 + 0.5];
160                    let d = segment_distance(p, [x0, y0], [x1, y1]);
161                    let k = (half + 0.5 - d).clamp(0.0, 1.0);
162                    let cell = &mut coverage[(py * w + px) as usize];
163                    *cell = cell.max(k);
164                }
165            }
166        }
167        Ok(coverage)
168    }
169
170    pub fn draw(&self, premultiplied: &mut [[f32; 4]], coverage: &[f32]) -> Result<(), String> {
171        if premultiplied.len() != coverage.len() {
172            return Err("the outline's coverage does not match the image".into());
173        }
174        for (texel, &cover) in premultiplied.iter_mut().zip(coverage) {
175            let k = (cover * self.alpha).clamp(0.0, 1.0);
176            for (value, colour) in texel.iter_mut().zip(self.color) {
177                *value = colour * k + *value * (1.0 - k);
178            }
179            texel[3] = k + texel[3] * (1.0 - k);
180        }
181        Ok(())
182    }
183}
184
185fn segment_distance(p: [f32; 2], a: [f32; 2], b: [f32; 2]) -> f32 {
186    let ab = [b[0] - a[0], b[1] - a[1]];
187    let ap = [p[0] - a[0], p[1] - a[1]];
188    let t = ((ap[0] * ab[0] + ap[1] * ab[1]) / (ab[0] * ab[0] + ab[1] * ab[1]).max(1e-12))
189        .clamp(0.0, 1.0);
190    let q = [a[0] + ab[0] * t - p[0], a[1] + ab[1] * t - p[1]];
191    (q[0] * q[0] + q[1] * q[1]).sqrt()
192}
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197
198    struct Fixture {
199        width: u32,
200        height: u32,
201        depth: Vec<f32>,
202        id: Vec<u32>,
203        normal: Vec<[f32; 3]>,
204    }
205
206    impl Fixture {
207        fn new(width: u32, height: u32) -> Self {
208            let n = (width * height) as usize;
209            Self {
210                width,
211                height,
212                depth: vec![0.0; n],
213                id: vec![0; n],
214                normal: vec![[0.0; 3]; n],
215            }
216        }
217
218        fn fill(
219            &mut self,
220            x: std::ops::Range<u32>,
221            y: std::ops::Range<u32>,
222            depth: f32,
223            id: u32,
224            normal: [f32; 3],
225        ) {
226            for yy in y {
227                for xx in x.clone() {
228                    let i = (yy * self.width + xx) as usize;
229                    self.depth[i] = depth;
230                    self.id[i] = id;
231                    self.normal[i] = normal;
232                }
233            }
234        }
235
236        fn g(&self) -> Gbuffer<'_> {
237            Gbuffer {
238                width: self.width,
239                height: self.height,
240                depth: &self.depth,
241                id: &self.id,
242                normal: &self.normal,
243            }
244        }
245    }
246
247    #[test]
248    fn a_square_gets_its_external_contour() {
249        let mut f = Fixture::new(32, 32);
250        f.fill(8..24, 8..24, 5.0, 1, [0.0, 0.0, 1.0]);
251        let lines = Lines {
252            enabled: true,
253            ..Lines::default()
254        };
255        let cover = lines.coverage(&f.g(), 1024).unwrap();
256        let at = |x: usize, y: usize| cover[y * 32 + x];
257        assert!((at(7, 16) - 1.0).abs() < 1e-6 && (at(8, 16) - 1.0).abs() < 1e-6);
258        assert_eq!((at(6, 16), at(9, 16)), (0.0, 0.0));
259        assert_eq!(at(16, 16), 0.0);
260        assert_eq!(at(2, 2), 0.0);
261        let thin = lines.coverage(&f.g(), 512).unwrap();
262        assert!((thin[16 * 32 + 7] - 0.5).abs() < 1e-6 && (thin[16 * 32 + 8] - 0.5).abs() < 1e-6);
263        let off = Lines {
264            external_contour: false,
265            silhouette: false,
266            ..lines
267        };
268        assert!(off.segments(&f.g()).unwrap().is_empty());
269    }
270
271    #[test]
272    fn creases_follow_the_crease_angle() {
273        let mut f = Fixture::new(16, 8);
274        f.fill(0..8, 0..8, 4.0, 1, [0.0, 0.0, 1.0]);
275        let tilt = 50.0f32.to_radians();
276        f.fill(8..16, 0..8, 4.0, 1, [tilt.sin(), 0.0, tilt.cos()]);
277        let g = f.g();
278        assert_eq!(
279            g.classify(7, 8, Some(6), Some(9), 134.0),
280            Some(Edge::Crease)
281        );
282        assert_eq!(g.classify(7, 8, Some(6), Some(9), 120.0), None);
283        let lines = Lines::default();
284        let segments = lines.segments(&g).unwrap();
285        assert_eq!(segments.len(), 8);
286        assert!(segments.iter().all(|s| s[0] == 8.0 && s[2] == 8.0));
287        assert!(
288            Lines {
289                crease: false,
290                ..lines
291            }
292            .segments(&g)
293            .unwrap()
294            .is_empty()
295        );
296    }
297
298    #[test]
299    fn depth_jumps_are_silhouettes_or_contours_and_slopes_are_not() {
300        let mut f = Fixture::new(12, 4);
301        for x in 0..12 {
302            f.fill(x..x + 1, 0..4, 2.0 + 0.3 * x as f32, 1, [0.0, 0.0, 1.0]);
303        }
304        let g = f.g();
305        assert_eq!(g.classify(4, 5, Some(3), Some(6), 134.0), None);
306        f.fill(6..12, 0..4, 9.0, 1, [0.0, 0.0, 1.0]);
307        assert_eq!(
308            f.g().classify(5, 6, Some(4), Some(7), 134.0),
309            Some(Edge::Contour)
310        );
311        f.fill(6..12, 0..4, 9.0, 2, [0.0, 0.0, 1.0]);
312        assert_eq!(
313            f.g().classify(5, 6, Some(4), Some(7), 134.0),
314            Some(Edge::Silhouette)
315        );
316        f.fill(6..12, 0..4, 2.0 + 0.3 * 6.0, 2, [0.0, 0.0, 1.0]);
317        assert_eq!(
318            f.g().classify(5, 6, Some(4), Some(7), 134.0),
319            Some(Edge::Border)
320        );
321        assert!(!Lines::default().draws(Edge::Contour));
322    }
323
324    #[test]
325    fn lines_draw_over_premultiplied_alpha() {
326        let lines = Lines {
327            color: [1.0, 0.0, 0.0],
328            alpha: 0.5,
329            ..Lines::default()
330        };
331        let mut image = vec![[0.0, 0.0, 0.4, 0.5], [0.0; 4]];
332        lines.draw(&mut image, &[1.0, 0.0]).unwrap();
333        assert_eq!(image[0], [0.5, 0.0, 0.2, 0.75]);
334        assert_eq!(image[1], [0.0; 4]);
335        assert!(lines.draw(&mut image, &[1.0]).is_err());
336        assert!((lines.width_px(2048) - 4.0).abs() < 1e-6);
337    }
338}