1use std::io::Write;
11
12use super::Mesh;
13
14const SS: usize = 2;
16
17pub fn png(mesh: &Mesh, size: u32) -> Option<Vec<u8>> {
20 if mesh.is_point_cloud() || size == 0 {
21 return None;
22 }
23 let rgba = draw(mesh, size as usize)?;
24 Some(encode_png(size, size, &rgba))
25}
26
27type V = [f64; 3];
28
29fn sub(a: V, b: V) -> V {
30 [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
31}
32
33fn dot(a: V, b: V) -> f64 {
34 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
35}
36
37fn cross(a: V, b: V) -> V {
38 [
39 a[1] * b[2] - a[2] * b[1],
40 a[2] * b[0] - a[0] * b[2],
41 a[0] * b[1] - a[1] * b[0],
42 ]
43}
44
45fn unit(a: V) -> V {
46 let l = dot(a, a).sqrt();
47 if l > 0.0 {
48 [a[0] / l, a[1] / l, a[2] / l]
49 } else {
50 a
51 }
52}
53
54fn draw(mesh: &Mesh, size: usize) -> Option<Vec<u8>> {
56 let w = size * SS;
57 let points: Vec<V> = mesh
58 .positions
59 .chunks_exact(3)
60 .map(|p| match *p {
61 [x, y, z] => [f64::from(x), f64::from(y), f64::from(z)],
62 _ => [0.0; 3],
63 })
64 .collect();
65 let b = mesh.bounds();
66 let centre = [
67 (f64::from(b[0]) + f64::from(b[3])) / 2.0,
68 (f64::from(b[1]) + f64::from(b[4])) / 2.0,
69 (f64::from(b[2]) + f64::from(b[5])) / 2.0,
70 ];
71 let view = unit([1.0, -1.0, 0.8]);
73 let right = unit(cross([0.0, 0.0, 1.0], view));
74 let up = cross(view, right);
75 let light = unit([
76 0.75 * view[0] + 0.45 * up[0] - 0.35 * right[0],
77 0.75 * view[1] + 0.45 * up[1] - 0.35 * right[1],
78 0.75 * view[2] + 0.45 * up[2] - 0.35 * right[2],
79 ]);
80 let projected: Vec<V> = points
82 .iter()
83 .map(|&p| {
84 let q = sub(p, centre);
85 [dot(q, right), dot(q, up), dot(q, view)]
86 })
87 .collect();
88 let (mut lo, mut hi) = ([f64::MAX; 3], [f64::MIN; 3]);
89 for p in &projected {
90 for ((l, h), &v) in lo.iter_mut().zip(hi.iter_mut()).zip(p) {
91 *l = l.min(v);
92 *h = h.max(v);
93 }
94 }
95 let half = ((hi[0] - lo[0]).max(hi[1] - lo[1]) / 2.0) * 1.08;
96 if !(half.is_finite() && half > 0.0) {
97 return None;
98 }
99 let mid = [(lo[0] + hi[0]) / 2.0, (lo[1] + hi[1]) / 2.0];
100 let scale = w as f64 / (2.0 * half);
101 let screen: Vec<V> = projected
102 .iter()
103 .map(|p| {
104 [
105 (p[0] - mid[0]) * scale + w as f64 / 2.0,
106 w as f64 / 2.0 - (p[1] - mid[1]) * scale,
107 p[2],
108 ]
109 })
110 .collect();
111
112 let mut depth = vec![f64::NEG_INFINITY; w * w];
113 let mut colour = vec![[0u8; 3]; w * w];
114 let has_colours = mesh.colors.len() == mesh.positions.len();
115 for t in mesh.indices.chunks_exact(3) {
116 let [ia, ib, ic] = match *t {
117 [a, b, c] => [a as usize, b as usize, c as usize],
118 _ => continue,
119 };
120 let (Some(&a), Some(&b), Some(&c)) = (screen.get(ia), screen.get(ib), screen.get(ic))
121 else {
122 continue;
123 };
124 let (Some(&pa), Some(&pb), Some(&pc)) = (points.get(ia), points.get(ib), points.get(ic))
125 else {
126 continue;
127 };
128 let normal = unit(cross(sub(pb, pa), sub(pc, pa)));
129 let shade = if dot(normal, view) >= 0.0 {
130 0.22 + 0.78 * dot(normal, light).clamp(0.0, 1.0)
131 } else {
132 0.12 };
134 let base = if has_colours {
135 let c = |i: usize, k: usize| f64::from(*mesh.colors.get(i * 3 + k).unwrap_or(&0));
136 [0, 1, 2].map(|k| (c(ia, k) + c(ib, k) + c(ic, k)) / 3.0)
137 } else {
138 [150.0, 170.0, 192.0]
139 };
140 let rgb = base.map(|v| (v * shade * 1.35).clamp(0.0, 255.0) as u8);
141 fill(&mut depth, &mut colour, w, [a, b, c], rgb);
142 }
143 if depth.iter().all(|d| d.is_infinite()) {
144 return None;
145 }
146 outline(&depth, &mut colour, w, hi[2] - lo[2]);
147 Some(downsample(&depth, &colour, w, size))
148}
149
150fn fill(depth: &mut [f64], colour: &mut [[u8; 3]], w: usize, [a, b, c]: [V; 3], rgb: [u8; 3]) {
152 let area = (b[1] - c[1]) * (a[0] - c[0]) + (c[0] - b[0]) * (a[1] - c[1]);
153 if area.abs() < 1e-12 || !area.is_finite() {
154 return;
155 }
156 let max = (w - 1) as f64;
157 let x0 = a[0].min(b[0]).min(c[0]).floor().clamp(0.0, max) as usize;
158 let x1 = a[0].max(b[0]).max(c[0]).ceil().clamp(0.0, max) as usize;
159 let y0 = a[1].min(b[1]).min(c[1]).floor().clamp(0.0, max) as usize;
160 let y1 = a[1].max(b[1]).max(c[1]).ceil().clamp(0.0, max) as usize;
161 for y in y0..=y1 {
162 let py = y as f64 + 0.5;
163 for x in x0..=x1 {
164 let px = x as f64 + 0.5;
165 let w0 = ((b[1] - c[1]) * (px - c[0]) + (c[0] - b[0]) * (py - c[1])) / area;
166 let w1 = ((c[1] - a[1]) * (px - c[0]) + (a[0] - c[0]) * (py - c[1])) / area;
167 let w2 = 1.0 - w0 - w1;
168 if w0 < -1e-9 || w1 < -1e-9 || w2 < -1e-9 {
169 continue;
170 }
171 let z = w0 * a[2] + w1 * b[2] + w2 * c[2];
172 let i = y * w + x;
173 if let (Some(d), Some(col)) = (depth.get_mut(i), colour.get_mut(i)) {
174 if z > *d {
175 *d = z;
176 *col = rgb;
177 }
178 }
179 }
180 }
181}
182
183fn outline(depth: &[f64], colour: &mut [[u8; 3]], w: usize, range: f64) {
185 let jump = range * 0.02;
186 let far = |d: f64| if d.is_finite() { d } else { -1e30 };
187 let mut edge = vec![false; w * w];
188 for y in 0..w {
189 for x in 0..w {
190 let i = y * w + x;
191 let d = far(*depth.get(i).unwrap_or(&f64::NEG_INFINITY));
192 let right = (x + 1 < w).then(|| far(*depth.get(i + 1).unwrap_or(&f64::NEG_INFINITY)));
193 let below = (y + 1 < w).then(|| far(*depth.get(i + w).unwrap_or(&f64::NEG_INFINITY)));
194 if [right, below]
195 .into_iter()
196 .flatten()
197 .any(|n| (n - d).abs() > jump)
198 {
199 if let Some(e) = edge.get_mut(i) {
200 *e = true;
201 }
202 }
203 }
204 }
205 for (c, e) in colour.iter_mut().zip(&edge) {
206 if *e {
207 *c = c.map(|v| (f64::from(v) * 0.35) as u8);
208 }
209 }
210}
211
212fn downsample(depth: &[f64], colour: &[[u8; 3]], w: usize, size: usize) -> Vec<u8> {
214 let mut out = Vec::with_capacity(size * size * 4);
215 for y in 0..size {
216 for x in 0..size {
217 let (mut sum, mut covered) = ([0u32; 3], 0u32);
218 for dy in 0..SS {
219 for dx in 0..SS {
220 let i = (y * SS + dy) * w + x * SS + dx;
221 if depth.get(i).is_some_and(|d| d.is_finite()) {
222 if let Some(c) = colour.get(i) {
223 for (s, &v) in sum.iter_mut().zip(c) {
224 *s += u32::from(v);
225 }
226 covered += 1;
227 }
228 }
229 }
230 }
231 if covered == 0 {
232 out.extend([0, 0, 0, 0]);
233 } else {
234 out.extend(sum.map(|s| (s / covered) as u8));
235 out.push((covered * 255 / (SS * SS) as u32) as u8);
236 }
237 }
238 }
239 out
240}
241
242fn encode_png(width: u32, height: u32, rgba: &[u8]) -> Vec<u8> {
244 let mut raw = Vec::with_capacity(rgba.len() + height as usize);
245 for row in rgba.chunks(width as usize * 4) {
246 raw.push(0); raw.extend_from_slice(row);
248 }
249 let mut z = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::default());
250 let compressed: Vec<u8> = z
252 .write_all(&raw)
253 .and_then(|_| z.finish())
254 .unwrap_or_default();
255 let mut png = vec![0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A];
256 let mut ihdr = Vec::with_capacity(13);
257 ihdr.extend(width.to_be_bytes());
258 ihdr.extend(height.to_be_bytes());
259 ihdr.extend([8, 6, 0, 0, 0]); chunk(&mut png, b"IHDR", &ihdr);
261 chunk(&mut png, b"IDAT", &compressed);
262 chunk(&mut png, b"IEND", &[]);
263 png
264}
265
266fn chunk(png: &mut Vec<u8>, kind: &[u8; 4], data: &[u8]) {
267 png.extend((data.len() as u32).to_be_bytes());
268 png.extend(kind);
269 png.extend(data);
270 let mut crc = flate2::Crc::new();
271 crc.update(kind);
272 crc.update(data);
273 png.extend(crc.sum().to_be_bytes());
274}
275
276#[cfg(test)]
277mod tests {
278 use super::*;
279
280 fn cube() -> Mesh {
281 let p = [
282 [0., 0., 0.],
283 [1., 0., 0.],
284 [1., 1., 0.],
285 [0., 1., 0.],
286 [0., 0., 1.],
287 [1., 0., 1.],
288 [1., 1., 1.],
289 [0., 1., 1.],
290 ];
291 let faces = [
292 [0, 2, 1],
293 [0, 3, 2],
294 [4, 5, 6],
295 [4, 6, 7],
296 [0, 1, 5],
297 [0, 5, 4],
298 [1, 2, 6],
299 [1, 6, 5],
300 [2, 3, 7],
301 [2, 7, 6],
302 [3, 0, 4],
303 [3, 4, 7],
304 ];
305 Mesh {
306 positions: p.iter().flatten().map(|&v: &f64| v as f32).collect(),
307 indices: faces.iter().flatten().copied().collect(),
308 parts: 1,
309 ..Mesh::default()
310 }
311 }
312
313 #[test]
314 fn a_cube_becomes_a_png_with_a_transparent_background() {
315 let png = png(&cube(), 64).unwrap();
316 assert!(png.starts_with(&[0x89, b'P', b'N', b'G']));
317 assert_eq!(png.get(16..24), Some(&[0, 0, 0, 64, 0, 0, 0, 64][..]));
319 assert_eq!(png.get(25), Some(&6));
320 let rgba = draw(&cube(), 64).unwrap();
321 let alpha: Vec<u8> = rgba.chunks(4).filter_map(|p| p.get(3).copied()).collect();
322 assert_eq!(alpha.first(), Some(&0), "corner is background");
323 assert_eq!(alpha.get(32 * 64 + 32), Some(&255), "centre is the cube");
324 let mut shades: Vec<&[u8]> = rgba
326 .chunks(4)
327 .filter(|p| p.get(3) == Some(&255))
328 .filter_map(|p| p.get(..3))
329 .collect();
330 shades.sort();
331 shades.dedup();
332 assert!(shades.len() >= 3, "{shades:?}");
333 }
334
335 #[test]
336 fn png_chunks_carry_valid_checksums() {
337 let png = encode_png(2, 1, &[255, 0, 0, 255, 0, 0, 255, 128]);
338 let mut at = 8;
339 let mut kinds = Vec::new();
340 let be =
341 |from: usize| u32::from_be_bytes(png.get(from..from + 4).unwrap().try_into().unwrap());
342 while at < png.len() {
343 let len = be(at) as usize;
344 let kind = png.get(at + 4..at + 8).unwrap();
345 let data = png.get(at + 8..at + 8 + len).unwrap();
346 let crc = be(at + 8 + len);
347 let mut c = flate2::Crc::new();
348 c.update(kind);
349 c.update(data);
350 assert_eq!(c.sum(), crc);
351 kinds.push(String::from_utf8_lossy(kind).to_string());
352 at += 12 + len;
353 }
354 assert_eq!(kinds, ["IHDR", "IDAT", "IEND"]);
355 }
356
357 #[test]
358 fn point_clouds_and_flat_nothing_give_no_image() {
359 let mut points = cube();
360 points.indices.clear();
361 assert!(png(&points, 32).is_none());
362 let empty = Mesh {
363 positions: vec![0.0; 9],
364 indices: vec![0, 1, 2],
365 ..Mesh::default()
366 };
367 assert!(png(&empty, 32).is_none());
368 }
369}