mesh files in: STL, OBJ, glTF and PLY
git clone https://git.lucas.co/cce-mesh-io.git
src/ply.rs (14K)
1 //! PLY (Stanford polygon file), ASCII and binary of either byte order.
2 //!
3 //! A PLY file declares its own layout: a header lists elements (`vertex`,
4 //! `face`, and anything else a tool chose to add), each with a count and
5 //! typed properties, and the body follows in that order. So the reader
6 //! parses the header into that layout and walks the body by it, keeping
7 //! `x y z` and any `red green blue` of each vertex and the index list of each
8 //! face, and reading past everything else — an unknown element still has to
9 //! be read to find where the next one starts.
10 //!
11 //! Colours stored as 8-bit (the usual) are sRGB and are decoded to linear;
12 //! colours stored as floats are taken as linear already. They ride on
13 //! corners. A file of points with no faces is an error for now: the viewer
14 //! draws no point clouds yet. The scene is [`UpAxis::Y`].
15
16 use glam::Vec3;
17
18 use crate::mesh::{srgb_to_linear, Material, Mesh};
19 use crate::{Part, Scene, Unit, UpAxis};
20
21 #[derive(Debug, Clone, Copy, PartialEq)]
22 enum Scalar {
23 I8,
24 U8,
25 I16,
26 U16,
27 I32,
28 U32,
29 F32,
30 F64,
31 }
32
33 impl Scalar {
34 fn parse(name: &str) -> Option<Scalar> {
35 Some(match name {
36 "char" | "int8" => Scalar::I8,
37 "uchar" | "uint8" => Scalar::U8,
38 "short" | "int16" => Scalar::I16,
39 "ushort" | "uint16" => Scalar::U16,
40 "int" | "int32" => Scalar::I32,
41 "uint" | "uint32" => Scalar::U32,
42 "float" | "float32" => Scalar::F32,
43 "double" | "float64" => Scalar::F64,
44 _ => return None,
45 })
46 }
47
48 fn size(self) -> usize {
49 match self {
50 Scalar::I8 | Scalar::U8 => 1,
51 Scalar::I16 | Scalar::U16 => 2,
52 Scalar::I32 | Scalar::U32 | Scalar::F32 => 4,
53 Scalar::F64 => 8,
54 }
55 }
56 }
57
58 #[derive(Debug)]
59 enum Property {
60 Scalar { name: String, ty: Scalar },
61 List { name: String, count: Scalar, item: Scalar },
62 }
63
64 #[derive(Debug)]
65 struct Element {
66 name: String,
67 count: usize,
68 properties: Vec<Property>,
69 }
70
71 #[derive(Debug, Clone, Copy, PartialEq)]
72 enum Encoding {
73 Ascii,
74 Little,
75 Big,
76 }
77
78 /// The body, read one value at a time in whichever encoding the file uses.
79 struct Body<'a> {
80 bytes: &'a [u8],
81 at: usize,
82 encoding: Encoding,
83 }
84
85 impl Body<'_> {
86 fn value(&mut self, ty: Scalar) -> Result<f64, String> {
87 if self.encoding == Encoding::Ascii {
88 return self.word().and_then(|w| w.parse::<f64>().map_err(|_| format!("'{w}' is not a number")));
89 }
90 let n = ty.size();
91 let raw = self.bytes.get(self.at..self.at + n).ok_or("the file ends in the middle of its data")?;
92 self.at += n;
93 let mut b = [0u8; 8];
94 b[..n].copy_from_slice(raw);
95 if self.encoding == Encoding::Big {
96 b[..n].reverse();
97 }
98 Ok(match ty {
99 Scalar::I8 => b[0] as i8 as f64,
100 Scalar::U8 => b[0] as f64,
101 Scalar::I16 => i16::from_le_bytes([b[0], b[1]]) as f64,
102 Scalar::U16 => u16::from_le_bytes([b[0], b[1]]) as f64,
103 Scalar::I32 => i32::from_le_bytes([b[0], b[1], b[2], b[3]]) as f64,
104 Scalar::U32 => u32::from_le_bytes([b[0], b[1], b[2], b[3]]) as f64,
105 Scalar::F32 => f32::from_le_bytes([b[0], b[1], b[2], b[3]]) as f64,
106 Scalar::F64 => f64::from_le_bytes(b),
107 })
108 }
109
110 fn word(&mut self) -> Result<&str, String> {
111 let rest = &self.bytes[self.at..];
112 let start = rest.iter().position(|c| !c.is_ascii_whitespace()).ok_or("the file ends in the middle of its data")?;
113 let len = rest[start..].iter().position(|c| c.is_ascii_whitespace()).unwrap_or(rest.len() - start);
114 self.at += start + len;
115 std::str::from_utf8(&rest[start..start + len]).map_err(|_| "text that is not UTF-8".to_string())
116 }
117 }
118
119 pub fn read(bytes: &[u8]) -> Result<Scene, String> {
120 let (elements, encoding, body_at) = header(bytes)?;
121 let mut body = Body { bytes, at: body_at, encoding };
122 let mut positions: Vec<Vec3> = Vec::new();
123 let mut point_colors: Vec<[f32; 3]> = Vec::new();
124 let mut faces: Vec<[u32; 3]> = Vec::new();
125 let mut corner: Vec<u32> = Vec::new();
126
127 for el in &elements {
128 match el.name.as_str() {
129 "vertex" => {
130 let at = |n: &str| {
131 el.properties.iter().position(|p| matches!(p, Property::Scalar { name, .. } if name == n))
132 };
133 let (Some(xi), Some(yi), Some(zi)) = (at("x"), at("y"), at("z")) else {
134 return Err("vertices without x, y and z".into());
135 };
136 let rgb = match (at("red"), at("green"), at("blue")) {
137 (Some(r), Some(g), Some(b)) => Some([r, g, b]),
138 _ => match (at("diffuse_red"), at("diffuse_green"), at("diffuse_blue")) {
139 (Some(r), Some(g), Some(b)) => Some([r, g, b]),
140 _ => None,
141 },
142 };
143 positions.reserve(el.count);
144 let mut row = vec![0f64; el.properties.len()];
145 for _ in 0..el.count {
146 for (i, p) in el.properties.iter().enumerate() {
147 row[i] = match p {
148 Property::Scalar { ty, .. } => body.value(*ty)?,
149 Property::List { count, item, .. } => {
150 skip_list(&mut body, *count, *item)?;
151 0.0
152 }
153 };
154 }
155 positions.push(Vec3::new(row[xi] as f32, row[yi] as f32, row[zi] as f32));
156 if let Some(idx) = rgb {
157 point_colors.push(idx.map(|i| match &el.properties[i] {
158 Property::Scalar { ty: Scalar::F32 | Scalar::F64, .. } => row[i] as f32,
159 _ => srgb_to_linear(row[i].clamp(0.0, 255.0) as u8),
160 }));
161 }
162 }
163 }
164 "face" => {
165 faces.reserve(el.count);
166 for _ in 0..el.count {
167 for p in &el.properties {
168 match p {
169 Property::List { name, count, item } if name == "vertex_indices" || name == "vertex_index" => {
170 let n = body.value(*count)? as usize;
171 corner.clear();
172 for _ in 0..n {
173 corner.push(body.value(*item)? as u32);
174 }
175 for k in 1..n.saturating_sub(1) {
176 faces.push([corner[0], corner[k], corner[k + 1]]);
177 }
178 }
179 Property::List { count, item, .. } => skip_list(&mut body, *count, *item)?,
180 Property::Scalar { ty, .. } => {
181 body.value(*ty)?;
182 }
183 }
184 }
185 }
186 }
187 _ => {
188 for _ in 0..el.count {
189 for p in &el.properties {
190 match p {
191 Property::Scalar { ty, .. } => {
192 body.value(*ty)?;
193 }
194 Property::List { count, item, .. } => skip_list(&mut body, *count, *item)?,
195 }
196 }
197 }
198 }
199 }
200 }
201
202 if faces.is_empty() {
203 return Err(if positions.is_empty() {
204 "no vertices and no faces".into()
205 } else {
206 format!("{} points and no faces: point clouds are not drawn yet", positions.len())
207 });
208 }
209 let n = positions.len() as u32;
210 if let Some(bad) = faces.iter().flatten().find(|&&i| i >= n) {
211 return Err(format!("a face names vertex {bad}, but there are {n}"));
212 }
213 let corner_colors = (!point_colors.is_empty())
214 .then(|| faces.iter().flat_map(|f| f.map(|i| point_colors[i as usize])).collect());
215 let mesh = Mesh {
216 tri_material: vec![0; faces.len()],
217 positions,
218 triangles: faces,
219 materials: vec![Material::default()],
220 corner_colors,
221 corner_uvs: None,
222 file_normals: None,
223 };
224 Ok(Scene { parts: vec![Part { name: String::new(), mesh }], up: UpAxis::Y, unit: Unit::Unspecified, textures: Vec::new() })
225 }
226
227 fn skip_list(body: &mut Body, count: Scalar, item: Scalar) -> Result<(), String> {
228 let n = body.value(count)? as usize;
229 for _ in 0..n {
230 body.value(item)?;
231 }
232 Ok(())
233 }
234
235 /// The elements, the encoding, and where the body starts.
236 fn header(bytes: &[u8]) -> Result<(Vec<Element>, Encoding, usize), String> {
237 if !bytes.starts_with(b"ply") {
238 return Err("not a PLY file (it does not begin with 'ply')".into());
239 }
240 let end = bytes
241 .windows(10)
242 .position(|w| w == b"end_header")
243 .ok_or("the header never ends (no 'end_header')")?;
244 // The body starts after the line end that follows end_header (\n or \r\n).
245 let mut body_at = end + 10;
246 while bytes.get(body_at).is_some_and(|&c| c == b' ' || c == b'\r') {
247 body_at += 1;
248 }
249 if bytes.get(body_at) == Some(&b'\n') {
250 body_at += 1;
251 }
252 let text = std::str::from_utf8(&bytes[..end]).map_err(|_| "a header that is not text".to_string())?;
253 let mut encoding = None;
254 let mut elements: Vec<Element> = Vec::new();
255 for line in text.lines().skip(1) {
256 let w: Vec<&str> = line.split_whitespace().collect();
257 match w.as_slice() {
258 ["format", "ascii", _] => encoding = Some(Encoding::Ascii),
259 ["format", "binary_little_endian", _] => encoding = Some(Encoding::Little),
260 ["format", "binary_big_endian", _] => encoding = Some(Encoding::Big),
261 ["format", other, ..] => return Err(format!("unknown PLY format '{other}'")),
262 ["element", name, count] => elements.push(Element {
263 name: name.to_string(),
264 count: count.parse().map_err(|_| format!("element {name} has count '{count}'"))?,
265 properties: Vec::new(),
266 }),
267 ["property", "list", count, item, name] => {
268 let el = elements.last_mut().ok_or("a property before any element")?;
269 let ty = |t: &str| Scalar::parse(t).ok_or_else(|| format!("unknown property type '{t}'"));
270 el.properties.push(Property::List { name: name.to_string(), count: ty(count)?, item: ty(item)? });
271 }
272 ["property", ty, name] => {
273 let el = elements.last_mut().ok_or("a property before any element")?;
274 let ty = Scalar::parse(ty).ok_or_else(|| format!("unknown property type '{ty}'"))?;
275 el.properties.push(Property::Scalar { name: name.to_string(), ty });
276 }
277 ["comment", ..] | ["obj_info", ..] | [] => {}
278 _ => log::debug!("[mesh-io] ply: skipped header line {line:?}"),
279 }
280 }
281 Ok((elements, encoding.ok_or("no 'format' line")?, body_at))
282 }
283
284 #[cfg(test)]
285 mod tests {
286 use super::*;
287
288 const ASCII_QUAD: &str = "ply\nformat ascii 1.0\ncomment made by hand\nelement vertex 4\nproperty float x\nproperty float y\nproperty float z\nproperty uchar red\nproperty uchar green\nproperty uchar blue\nelement face 1\nproperty list uchar int vertex_indices\nend_header\n0 0 0 255 0 0\n1 0 0 255 0 0\n1 1 0 0 0 255\n0 1 0 0 0 255\n4 0 1 2 3\n";
289
290 fn mesh(bytes: &[u8]) -> Mesh {
291 read(bytes).unwrap().parts.remove(0).mesh
292 }
293
294 #[test]
295 fn an_ascii_quad_is_two_coloured_triangles() {
296 let m = mesh(ASCII_QUAD.as_bytes());
297 assert_eq!(m.triangles, vec![[0, 1, 2], [0, 2, 3]]);
298 let c = m.corner_colors.unwrap();
299 assert_eq!(c[0], [1.0, 0.0, 0.0]);
300 assert_eq!(c[2], [0.0, 0.0, 1.0], "corner 2 is vertex 2, blue");
301 }
302
303 /// The quad again, binary, with an extra element in the way and a
304 /// per-face list it must read past.
305 fn binary_quad(big: bool) -> Vec<u8> {
306 let fmt = if big { "binary_big_endian" } else { "binary_little_endian" };
307 let mut b = format!(
308 "ply\r\nformat {fmt} 1.0\r\nelement vertex 4\r\nproperty double x\r\nproperty double y\r\nproperty double z\r\nelement face 1\r\nproperty list uchar uint vertex_indices\r\nproperty list uchar float texcoord\r\nelement extra 1\r\nproperty short junk\r\nend_header\r\n"
309 )
310 .into_bytes();
311 let f64b = |v: f64| if big { v.to_be_bytes() } else { v.to_le_bytes() };
312 let u32b = |v: u32| if big { v.to_be_bytes() } else { v.to_le_bytes() };
313 for p in [[0., 0., 0.], [1., 0., 0.], [1., 1., 0.], [0., 1., 0.]] {
314 for c in p {
315 b.extend(f64b(c));
316 }
317 }
318 b.push(4);
319 for i in [0, 1, 2, 3] {
320 b.extend(u32b(i));
321 }
322 b.push(2);
323 b.extend([0u8; 8]);
324 b.extend([7u8, 7]);
325 b
326 }
327
328 #[test]
329 fn binary_of_either_byte_order() {
330 for big in [false, true] {
331 let m = mesh(&binary_quad(big));
332 assert_eq!(m.triangles, vec![[0, 1, 2], [0, 2, 3]], "big endian: {big}");
333 assert_eq!(m.positions[2], Vec3::new(1.0, 1.0, 0.0));
334 assert!(m.corner_colors.is_none());
335 }
336 }
337
338 #[test]
339 fn a_point_cloud_says_so() {
340 let text = "ply\nformat ascii 1.0\nelement vertex 2\nproperty float x\nproperty float y\nproperty float z\nend_header\n0 0 0\n1 1 1\n";
341 let e = read(text.as_bytes()).unwrap_err();
342 assert!(e.contains("point clouds"), "{e}");
343 }
344
345 #[test]
346 fn a_short_file_and_a_bad_index_are_errors() {
347 let mut b = binary_quad(false);
348 b.truncate(b.len() - 20);
349 assert!(read(&b).unwrap_err().contains("ends in the middle"));
350 let bad = ASCII_QUAD.replace("4 0 1 2 3", "3 0 1 9");
351 assert!(read(bad.as_bytes()).unwrap_err().contains("names vertex 9"));
352 }
353 }