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}