Skip to main content

falcon_library/model3d/
ply.rs

1//! PLY, ASCII and binary little-endian, with vertex colours.
2
3use super::math::V3;
4use super::{Mesh, Model, ParseError, Result};
5
6#[derive(Clone, Copy, PartialEq)]
7enum Ty {
8    I8,
9    U8,
10    I16,
11    U16,
12    I32,
13    U32,
14    F32,
15    F64,
16}
17
18impl Ty {
19    fn from_name(s: &str) -> Option<Ty> {
20        Some(match s {
21            "char" | "int8" => Ty::I8,
22            "uchar" | "uint8" => Ty::U8,
23            "short" | "int16" => Ty::I16,
24            "ushort" | "uint16" => Ty::U16,
25            "int" | "int32" => Ty::I32,
26            "uint" | "uint32" => Ty::U32,
27            "float" | "float32" => Ty::F32,
28            "double" | "float64" => Ty::F64,
29            _ => return None,
30        })
31    }
32
33    fn size(self) -> usize {
34        match self {
35            Ty::I8 | Ty::U8 => 1,
36            Ty::I16 | Ty::U16 => 2,
37            Ty::I32 | Ty::U32 | Ty::F32 => 4,
38            Ty::F64 => 8,
39        }
40    }
41
42    /// What a colour channel of this type divides by to reach 0..1.
43    fn color_scale(self) -> f64 {
44        match self {
45            Ty::U8 | Ty::I8 => 255.0,
46            Ty::U16 | Ty::I16 => 65535.0,
47            Ty::U32 | Ty::I32 => 4294967295.0,
48            Ty::F32 | Ty::F64 => 1.0,
49        }
50    }
51}
52
53struct Prop {
54    name: String,
55    ty: Ty,
56    /// The type of the count when this is a list.
57    list: Option<Ty>,
58}
59
60struct Element {
61    name: String,
62    count: usize,
63    props: Vec<Prop>,
64}
65
66#[derive(Clone, Copy, PartialEq)]
67enum Format {
68    Ascii,
69    Little,
70    Big,
71}
72
73struct Header {
74    format: Format,
75    elements: Vec<Element>,
76    /// Where the data starts.
77    body: usize,
78}
79
80fn parse_header(bytes: &[u8]) -> Result<Header> {
81    let mut pos = 0;
82    let mut format = None;
83    let mut elements: Vec<Element> = Vec::new();
84    let mut first = true;
85    loop {
86        if pos >= bytes.len() {
87            return Err(ParseError::new("PLY header has no end_header"));
88        }
89        let end = bytes[pos..].iter().position(|&b| b == b'\n').map_or(bytes.len(), |i| pos + i);
90        let line = String::from_utf8_lossy(&bytes[pos..end]).into_owned();
91        pos = (end + 1).min(bytes.len());
92        let mut w = line.split_whitespace();
93        let Some(key) = w.next() else { continue };
94        if first {
95            if key != "ply" {
96                return Err(ParseError::new("not a PLY file"));
97            }
98            first = false;
99            continue;
100        }
101        let bad = || ParseError(format!("bad PLY header line: {}", line.trim()));
102        match key {
103            "format" => {
104                format = Some(match w.next() {
105                    Some("ascii") => Format::Ascii,
106                    Some("binary_little_endian") => Format::Little,
107                    Some("binary_big_endian") => Format::Big,
108                    _ => return Err(bad()),
109                });
110            }
111            "element" => {
112                let name = w.next().ok_or_else(bad)?.to_string();
113                let count = w.next().and_then(|c| c.parse().ok()).ok_or_else(bad)?;
114                elements.push(Element { name, count, props: Vec::new() });
115            }
116            "property" => {
117                let el = elements.last_mut().ok_or_else(bad)?;
118                let t = w.next().ok_or_else(bad)?;
119                if t == "list" {
120                    let ct = w.next().and_then(Ty::from_name).ok_or_else(bad)?;
121                    let it = w.next().and_then(Ty::from_name).ok_or_else(bad)?;
122                    let name = w.next().ok_or_else(bad)?.to_string();
123                    el.props.push(Prop { name, ty: it, list: Some(ct) });
124                } else {
125                    let ty = Ty::from_name(t).ok_or_else(bad)?;
126                    let name = w.next().ok_or_else(bad)?.to_string();
127                    el.props.push(Prop { name, ty, list: None });
128                }
129            }
130            "end_header" => break,
131            _ => {} // comment, obj_info
132        }
133    }
134    let format = format.ok_or_else(|| ParseError::new("PLY header has no format"))?;
135    Ok(Header { format, elements, body: pos })
136}
137
138/// Reads values one at a time from the data section.
139struct Src<'a> {
140    data: &'a [u8],
141    pos: usize,
142    format: Format,
143}
144
145impl Src<'_> {
146    #[inline]
147    fn read(&mut self, ty: Ty) -> Option<f64> {
148        if self.format == Format::Ascii {
149            return self.read_ascii();
150        }
151        let n = ty.size();
152        let s = self.data.get(self.pos..self.pos + n)?;
153        self.pos += n;
154        let big = self.format == Format::Big;
155        macro_rules! num {
156            ($t:ty) => {{
157                let a = s.try_into().ok()?;
158                (if big { <$t>::from_be_bytes(a) } else { <$t>::from_le_bytes(a) }) as f64
159            }};
160        }
161        Some(match ty {
162            Ty::I8 => s[0] as i8 as f64,
163            Ty::U8 => s[0] as f64,
164            Ty::I16 => num!(i16),
165            Ty::U16 => num!(u16),
166            Ty::I32 => num!(i32),
167            Ty::U32 => num!(u32),
168            Ty::F32 => num!(f32),
169            Ty::F64 => num!(f64),
170        })
171    }
172
173    fn read_ascii(&mut self) -> Option<f64> {
174        let d = self.data;
175        let mut p = self.pos;
176        while p < d.len() && d[p].is_ascii_whitespace() {
177            p += 1;
178        }
179        let start = p;
180        while p < d.len() && !d[p].is_ascii_whitespace() {
181            p += 1;
182        }
183        self.pos = p;
184        std::str::from_utf8(&d[start..p]).ok()?.parse().ok()
185    }
186}
187
188fn eof() -> ParseError {
189    ParseError::new("PLY data ends early or is malformed")
190}
191
192pub fn parse(bytes: &[u8]) -> Result<Model> {
193    let h = parse_header(bytes)?;
194    let mut src = Src { data: bytes, pos: h.body, format: h.format };
195    let mut mesh = Mesh::default();
196    let mut have_vertex = false;
197    for el in &h.elements {
198        match el.name.as_str() {
199            "vertex" if !have_vertex => {
200                have_vertex = true;
201                read_vertices(&mut src, el, &mut mesh)?;
202            }
203            "face" => read_faces(&mut src, el, &mut mesh)?,
204            _ => skip_element(&mut src, el)?,
205        }
206    }
207    Model { meshes: vec![mesh] }.finish()
208}
209
210fn skip_element(src: &mut Src, el: &Element) -> Result<()> {
211    if src.format != Format::Ascii && el.props.iter().all(|p| p.list.is_none()) {
212        let row: usize = el.props.iter().map(|p| p.ty.size()).sum();
213        src.pos = row.checked_mul(el.count).and_then(|n| src.pos.checked_add(n)).filter(|&e| e <= src.data.len()).ok_or_else(eof)?;
214        return Ok(());
215    }
216    for _ in 0..el.count {
217        for p in &el.props {
218            skip_prop(src, p)?;
219        }
220    }
221    Ok(())
222}
223
224fn skip_prop(src: &mut Src, p: &Prop) -> Result<()> {
225    match p.list {
226        None => {
227            src.read(p.ty).ok_or_else(eof)?;
228        }
229        Some(ct) => {
230            let n = src.read(ct).ok_or_else(eof)? as usize;
231            if src.format == Format::Ascii {
232                for _ in 0..n {
233                    src.read(p.ty).ok_or_else(eof)?;
234                }
235            } else {
236                src.pos = src.pos.saturating_add(n.saturating_mul(p.ty.size()));
237                if src.pos > src.data.len() {
238                    return Err(eof());
239                }
240            }
241        }
242    }
243    Ok(())
244}
245
246fn read_vertices(src: &mut Src, el: &Element, mesh: &mut Mesh) -> Result<()> {
247    let find = |n: &str| el.props.iter().position(|p| p.name == n && p.list.is_none());
248    let (Some(x), Some(y), Some(z)) = (find("x"), find("y"), find("z")) else {
249        return Err(ParseError::new("PLY vertices have no x, y, z"));
250    };
251    let normal = find("nx").zip(find("ny")).zip(find("nz")).map(|((a, b), c)| [a, b, c]);
252    let color = find("red").zip(find("green")).zip(find("blue")).map(|((a, b), c)| [a, b, c]);
253    let cap = el.count.min(src.data.len());
254    mesh.positions.reserve(cap);
255    let mut normals: Vec<V3> = Vec::with_capacity(if normal.is_some() { cap } else { 0 });
256    let mut colors: Vec<V3> = Vec::with_capacity(if color.is_some() { cap } else { 0 });
257    let mut row = vec![0.0f64; el.props.len()];
258    for _ in 0..el.count {
259        for (i, p) in el.props.iter().enumerate() {
260            if p.list.is_some() {
261                skip_prop(src, p)?;
262            } else {
263                row[i] = src.read(p.ty).ok_or_else(eof)?;
264            }
265        }
266        mesh.positions.push([row[x] as f32, row[y] as f32, row[z] as f32]);
267        if let Some(n) = normal {
268            normals.push(n.map(|i| row[i] as f32));
269        }
270        if let Some(c) = color {
271            colors.push(c.map(|i| (row[i] / el.props[i].ty.color_scale()) as f32));
272        }
273    }
274    mesh.normals = normal.map(|_| normals);
275    mesh.colors = color.map(|_| colors);
276    Ok(())
277}
278
279fn read_faces(src: &mut Src, el: &Element, mesh: &mut Mesh) -> Result<()> {
280    mesh.indices.reserve(el.count.min(src.data.len()) * 3);
281    let mut poly: Vec<u32> = Vec::new();
282    for _ in 0..el.count {
283        for p in &el.props {
284            let is_idx = p.list.is_some() && (p.name == "vertex_indices" || p.name == "vertex_index");
285            if !is_idx {
286                skip_prop(src, p)?;
287                continue;
288            }
289            let n = src.read(p.list.unwrap_or(Ty::U8)).ok_or_else(eof)? as usize;
290            poly.clear();
291            let mut ok = true;
292            for _ in 0..n {
293                let v = src.read(p.ty).ok_or_else(eof)?;
294                if v < 0.0 || v > u32::MAX as f64 {
295                    ok = false;
296                }
297                poly.push(v as u32);
298            }
299            if ok {
300                for k in 1..poly.len().saturating_sub(1) {
301                    mesh.indices.extend([poly[0], poly[k], poly[k + 1]]);
302                }
303            }
304        }
305    }
306    Ok(())
307}
308
309#[cfg(test)]
310mod tests {
311    use super::*;
312
313    const SQUARE: &str = "ply\nformat ascii 1.0\ncomment square\nelement vertex 4\nproperty float x\nproperty float y\nproperty float z\n\
314                          element face 1\nproperty list uchar int vertex_indices\nend_header\n0 0 0\n1 0 0\n1 1 0\n0 1 0\n4 0 1 2 3\n";
315
316    #[test]
317    fn ascii_quad_is_two_triangles() {
318        let m = parse(SQUARE.as_bytes()).unwrap();
319        assert_eq!(m.meshes.len(), 1);
320        assert_eq!(m.triangle_count(), 2);
321        assert_eq!(m.vertex_count(), 4);
322        assert_eq!(m.meshes[0].indices, vec![0, 1, 2, 0, 2, 3]);
323        assert!(m.meshes[0].colors.is_none() && m.meshes[0].normals.is_none());
324    }
325
326    #[test]
327    fn crlf_header() {
328        let m = parse(SQUARE.replace('\n', "\r\n").as_bytes()).unwrap();
329        assert_eq!(m.triangle_count(), 2);
330    }
331
332    fn binary_with_unknown_element(big: bool) -> Vec<u8> {
333        let fmt = if big { "binary_big_endian" } else { "binary_little_endian" };
334        let mut out = format!(
335            "ply\nformat {fmt} 1.0\nelement vertex 3\nproperty float x\nproperty float y\nproperty float z\nproperty float nx\nproperty float ny\nproperty float nz\n\
336             property uchar red\nproperty uchar green\nproperty uchar blue\nproperty uchar alpha\n\
337             element junk 2\nproperty uchar tag\nproperty list uchar short things\n\
338             element face 1\nproperty list uchar uint vertex_indices\nproperty float quality\nend_header\n"
339        )
340        .into_bytes();
341        let f = |v: f32| if big { v.to_be_bytes() } else { v.to_le_bytes() };
342        let pts = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
343        for (i, p) in pts.iter().enumerate() {
344            for c in p {
345                out.extend(f(*c));
346            }
347            for c in [0.0, 0.0, 1.0] {
348                out.extend(f(c));
349            }
350            out.extend([255, 0, 51 * i as u8, 255]);
351        }
352        out.extend([9, 3, 1, 0, 2, 0, 3, 0]); // tag 9, list of 3 shorts (LE)
353        out.extend([8, 0]); // tag 8, empty list
354        out.push(3);
355        for i in [0u32, 1, 2] {
356            out.extend(if big { i.to_be_bytes() } else { i.to_le_bytes() });
357        }
358        out.extend(f(0.5));
359        out
360    }
361
362    #[test]
363    fn binary_little_endian_colours_normals_and_skipped_element() {
364        let bytes = binary_with_unknown_element(false);
365        // The short list payload is little endian; fine for the LE file.
366        let m = parse(&bytes).unwrap();
367        let mesh = &m.meshes[0];
368        assert_eq!(m.triangle_count(), 1);
369        assert_eq!(mesh.positions[1], [1.0, 0.0, 0.0]);
370        assert_eq!(mesh.normals.as_ref().unwrap()[2], [0.0, 0.0, 1.0]);
371        let c = mesh.colors.as_ref().unwrap();
372        assert_eq!(c[0], [1.0, 0.0, 0.0]);
373        assert!((c[2][2] - 102.0 / 255.0).abs() < 1e-6);
374    }
375
376    #[test]
377    fn binary_big_endian() {
378        let mut bytes = binary_with_unknown_element(true);
379        // Make the short-list payload big endian too (values 1, 2, 3).
380        let at = bytes.windows(8).position(|w| w == [9, 3, 1, 0, 2, 0, 3, 0]).unwrap();
381        bytes[at + 1..at + 8].copy_from_slice(&[3, 0, 1, 0, 2, 0, 3]);
382        let m = parse(&bytes).unwrap();
383        assert_eq!(m.triangle_count(), 1);
384        assert_eq!(m.meshes[0].positions[2], [0.0, 1.0, 0.0]);
385    }
386
387    #[test]
388    fn ascii_float_colours() {
389        let src = "ply\nformat ascii 1.0\nelement vertex 3\nproperty float x\nproperty float y\nproperty float z\n\
390                   property float red\nproperty float green\nproperty float blue\nelement face 1\nproperty list uchar int vertex_index\nend_header\n\
391                   0 0 0 1 0 0\n1 0 0 0 0.5 0\n0 1 0 0 0 1\n3 0 1 2\n";
392        let m = parse(src.as_bytes()).unwrap();
393        let c = m.meshes[0].colors.as_ref().unwrap();
394        assert_eq!(c[0], [1.0, 0.0, 0.0]);
395        assert_eq!(c[1], [0.0, 0.5, 0.0]);
396    }
397
398    #[test]
399    fn point_cloud_is_an_error() {
400        let src = "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";
401        assert!(parse(src.as_bytes()).is_err());
402    }
403
404    #[test]
405    fn bad_input_is_an_error() {
406        assert!(parse(b"hello").is_err());
407        assert!(parse(b"ply\nformat ascii 1.0\nelement vertex 1\n").is_err());
408        // Truncated body.
409        let b = binary_with_unknown_element(false);
410        assert!(parse(&b[..b.len() - 10]).is_err());
411    }
412
413    #[test]
414    fn out_of_range_faces_are_dropped() {
415        let src = SQUARE.replace("4 0 1 2 3", "3 0 1 9");
416        assert!(parse(src.as_bytes()).is_err());
417    }
418}