gloss_renderer/
builders.rs

1#![allow(clippy::missing_panics_doc)] //a lot of operations require inserting or removing component from a entity but
2                                      // the entity will for sure exists so it will never panic
3
4use crate::components::{Colors, DiffuseImg, Edges, ImgConfig};
5use gloss_hecs::EntityBuilder;
6use gloss_utils::tensor::{DynamicMatrixOps, DynamicTensorFloat2D, DynamicTensorInt2D};
7use log::debug;
8use nalgebra_glm::{Vec2, Vec3};
9
10use crate::components::{Faces, ModelMatrix, Normals, UVs, Verts};
11use core::f32;
12use gloss_utils::io::FileType;
13#[allow(unused_imports)]
14use log::{error, info, warn};
15use na::DMatrix;
16use nalgebra as na;
17use std::path::Path;
18use tobj;
19
20use ply_rs::{parser, ply};
21
22/// Creates a cube
23#[must_use]
24pub fn build_cube(center: na::Point3<f32>, scale: f32) -> EntityBuilder {
25    //makes a 1x1x1 vox in NDC. which has Z going into the screen
26    let mut verts = DMatrix::<f32>::from_row_slice(
27        8,
28        3,
29        &[
30            //behind face (which has negative Z as the camera is now looking in the positive Z direction and this will be the face that is
31            // behind the camera)
32            -1.0, -1.0, -1.0, //bottom-left
33            1.0, -1.0, -1.0, //bottom-right
34            1.0, 1.0, -1.0, //top-right
35            -1.0, 1.0, -1.0, //top-left
36            //front face
37            -1.0, -1.0, 1.0, //bottom-left
38            1.0, -1.0, 1.0, //bottom-right
39            1.0, 1.0, 1.0, //top-right
40            -1.0, 1.0, 1.0, //top-left
41        ],
42    );
43    verts *= scale;
44
45    //faces (2 triangles per faces, with 6 faces which makes 12 triangles)
46    let faces = DMatrix::<u32>::from_row_slice(
47        12,
48        3,
49        &[
50            2, 1, 0, //
51            2, 0, 3, //
52            4, 5, 6, //
53            7, 4, 6, //
54            5, 0, 1, //
55            4, 0, 5, //
56            7, 6, 3, //
57            3, 6, 2, //
58            3, 0, 4, //
59            3, 4, 7, //
60            6, 5, 1, //
61            6, 1, 2, //
62        ],
63    );
64
65    //since we want each vertex at the corner to have multiple normals, we just
66    // duplicate them
67    let mut verts_dup = Vec::new();
68    let mut faces_dup = Vec::new();
69    for f in faces.row_iter() {
70        let p1 = verts.row(f[0] as usize);
71        let p2 = verts.row(f[2] as usize);
72        let p3 = verts.row(f[1] as usize);
73        verts_dup.push(p1);
74        verts_dup.push(p2);
75        verts_dup.push(p3);
76
77        #[allow(clippy::cast_possible_truncation)]
78        let v_size = verts_dup.len() as u32;
79        // let new_face = na::RowDVector::<u32>::new(v_size-1, v_size-2,
80        // v_size-3);//corresponds to p3,p2 and p1
81        let new_face = na::RowDVector::<u32>::from_row_slice(&[v_size - 1, v_size - 2, v_size - 3]); //corresponds to p3,p2 and p1
82        faces_dup.push(new_face);
83    }
84
85    let verts = na::DMatrix::<f32>::from_rows(verts_dup.as_slice());
86    let faces = na::DMatrix::<u32>::from_rows(faces_dup.as_slice());
87
88    let mut model_matrix = na::SimilarityMatrix3::<f32>::identity();
89    // model_matrix.append_scaling_mut(0.1);
90    model_matrix.append_translation_mut(&center.coords.into());
91    let verts_tensor = DynamicTensorFloat2D::from_dmatrix(&verts);
92    let faces_tensor = DynamicTensorInt2D::from_dmatrix(&faces);
93
94    let mut builder = EntityBuilder::new();
95    builder.add(Verts(verts_tensor)).add(Faces(faces_tensor)).add(ModelMatrix(model_matrix));
96
97    builder
98}
99
100/// Creates a plane based on a center and normal. If `transform_cpu_data` is
101/// true, then the vertices of the plane are actually translated and rotated
102/// on the CPU. Otherwise the transformation is done on the GPU using the
103/// model matrix. This is useful when creating light planes for example when
104/// we want the model matrix to be consistent with the orientation of the
105/// light and therefore we set `transform_cpu_data=false`
106#[must_use]
107pub fn build_plane(center: na::Point3<f32>, normal: na::Vector3<f32>, size_x: f32, size_y: f32, transform_cpu_data: bool) -> EntityBuilder {
108    //make 4 vertices
109    let mut verts = DMatrix::<f32>::from_row_slice(
110        4,
111        3,
112        &[
113            -1.0 * size_x,
114            0.0,
115            -1.0 * size_y, //
116            1.0 * size_x,
117            0.0,
118            -1.0 * size_y, //
119            1.0 * size_x,
120            0.0,
121            1.0 * size_y, //
122            -1.0 * size_x,
123            0.0,
124            1.0 * size_y, //
125        ],
126    );
127    //make 2 faces
128    let faces = DMatrix::<u32>::from_row_slice(
129        2,
130        3,
131        &[
132            2, 1, 0, //
133            3, 2, 0,
134        ],
135    );
136
137    //uvs
138    let uvs = DMatrix::<f32>::from_row_slice(
139        4,
140        2,
141        &[
142            0.0, 0.0, //
143            1.0, 0.0, //
144            1.0, 1.0, //
145            0.0, 1.0, //
146        ],
147    );
148
149    //make a model matrix
150    let up = na::Vector3::<f32>::new(0.0, 1.0, 0.0);
151    let lookat = center + normal * 1.0;
152    //up and normal are colinear so face_towards would fail, we just set to
153    // identity
154    let mut model_matrix = if up.angle(&normal.normalize()) < 1e-6 {
155        let mut mm = na::SimilarityMatrix3::<f32>::identity();
156        mm.append_translation_mut(&na::Translation3::from(center));
157        mm
158    } else {
159        let mut m = na::SimilarityMatrix3::<f32>::face_towards(&center, &lookat, &up, 1.0);
160        m = m
161            * na::Rotation3::<f32>::from_axis_angle(&na::Vector3::z_axis(), std::f32::consts::FRAC_PI_2)
162            * na::Rotation3::<f32>::from_axis_angle(&na::Vector3::x_axis(), std::f32::consts::FRAC_PI_2); //rotate 90 degrees
163        m
164    };
165
166    if transform_cpu_data {
167        //transform directly the verts
168        for mut vert in verts.row_iter_mut() {
169            let v_modif = model_matrix * na::Point3::from(vert.fixed_columns::<3>(0).transpose());
170            vert.copy_from_slice(v_modif.coords.as_slice());
171        }
172        //reset to identity
173        model_matrix = na::SimilarityMatrix3::<f32>::identity();
174    }
175    let verts_tensor = DynamicTensorFloat2D::from_dmatrix(&verts);
176    let faces_tensor = DynamicTensorInt2D::from_dmatrix(&faces);
177    let uvs_tensor = DynamicTensorFloat2D::from_dmatrix(&uvs);
178
179    let mut builder = EntityBuilder::new();
180    builder
181        .add(Verts(verts_tensor))
182        .add(Faces(faces_tensor))
183        .add(UVs(uvs_tensor))
184        .add(ModelMatrix(model_matrix));
185
186    builder
187}
188
189#[must_use]
190pub fn build_floor() -> EntityBuilder {
191    //make 4 vertices
192    let verts = DMatrix::<f32>::from_row_slice(
193        4,
194        3,
195        &[
196            -1.0, 0.0, -1.0, //
197            1.0, 0.0, -1.0, //
198            1.0, 0.0, 1.0, //
199            -1.0, 0.0, 1.0, //
200        ],
201    );
202    //make 2 faces
203    let faces = DMatrix::<u32>::from_row_slice(
204        2,
205        3,
206        &[
207            2, 1, 0, //
208            3, 2, 0,
209        ],
210    );
211
212    //uvs
213    let uvs = DMatrix::<f32>::from_row_slice(
214        4,
215        2,
216        &[
217            0.0, 0.0, //
218            1.0, 0.0, //
219            1.0, 1.0, //
220            0.0, 1.0, //
221        ],
222    );
223    let verts_tensor = DynamicTensorFloat2D::from_dmatrix(&verts);
224    let faces_tensor = DynamicTensorInt2D::from_dmatrix(&faces);
225    let uvs_tensor = DynamicTensorFloat2D::from_dmatrix(&uvs);
226    let mut builder = EntityBuilder::new();
227    builder.add(Verts(verts_tensor)).add(Faces(faces_tensor)).add(UVs(uvs_tensor));
228
229    builder
230}
231
232#[must_use]
233#[allow(clippy::cast_precision_loss)]
234pub fn build_grid(
235    center: na::Point3<f32>,
236    normal: na::Vector3<f32>,
237    nr_lines_x: u32,
238    nr_lines_y: u32,
239    size_x: f32,
240    size_y: f32,
241    transform_cpu_data: bool,
242) -> EntityBuilder {
243    //a grid has at least 2 lines in each dimension, because each square needs 2
244    // lines, so we cap the number
245    let nr_lines_x = nr_lines_x.max(2);
246    let nr_lines_y = nr_lines_y.max(2);
247
248    //in order to be consistent with build_plane we multiply the size in x and y by
249    // 2 since it's technically the size from the center until the outer edge
250    // let size_x = size_x * 2.0;
251    // let size_y = size_y * 2.0;
252
253    let size_cell_x = size_x / nr_lines_x as f32;
254    let size_cell_y = size_y / nr_lines_y as f32;
255    let grid_half_size_x = size_x / 2.0;
256    let grid_half_size_y = size_y / 2.0;
257
258    // println!("nr_linex_x {}", size_cell_x);
259    // println!("size_cell_x {}", size_cell_x);
260    // println!("grid_half_size_x {}", grid_half_size_x);
261
262    //make points
263    let mut verts = Vec::new();
264    for idx_y in 0..nr_lines_y {
265        for idx_x in 0..nr_lines_x {
266            verts.push(idx_x as f32 * size_cell_x - grid_half_size_x);
267            verts.push(0.0);
268            verts.push(idx_y as f32 * size_cell_y - grid_half_size_y);
269        }
270    }
271
272    //make edges horizontally
273    let mut edges_h = Vec::new();
274    for idx_y in 0..nr_lines_y {
275        for idx_x in 0..nr_lines_x - 1 {
276            let idx_cur = idx_y * nr_lines_x + idx_x;
277            let idx_next = idx_y * nr_lines_x + idx_x + 1;
278            edges_h.push(idx_cur);
279            edges_h.push(idx_next);
280        }
281    }
282
283    //make edges vertically
284    let mut edges_v = Vec::new();
285    for idx_y in 0..nr_lines_y - 1 {
286        for idx_x in 0..nr_lines_x {
287            let idx_cur = idx_y * nr_lines_x + idx_x;
288            let idx_next = (idx_y + 1) * nr_lines_x + idx_x;
289            edges_v.push(idx_cur);
290            edges_v.push(idx_next);
291        }
292    }
293
294    let mut edges = edges_h;
295    edges.extend(edges_v);
296
297    //make nalgebra matrices
298    let mut verts = DMatrix::<f32>::from_row_slice(verts.len() / 3, 3, &verts);
299    let edges = DMatrix::<u32>::from_row_slice(edges.len() / 2, 2, &edges);
300
301    //make a model matrix
302    let up = na::Vector3::<f32>::new(0.0, 1.0, 0.0);
303    let lookat = center + normal * 1.0;
304    //up and normal are colinear so face_towards would fail, we just set to
305    // identity
306    let mut model_matrix = if up.angle(&normal.normalize()) < 1e-6 {
307        let mut mm = na::SimilarityMatrix3::<f32>::identity();
308        mm.append_translation_mut(&na::Translation3::from(center));
309        mm
310    } else {
311        let mut m = na::SimilarityMatrix3::<f32>::face_towards(&center, &lookat, &up, 1.0);
312        m = m
313            * na::Rotation3::<f32>::from_axis_angle(&na::Vector3::z_axis(), std::f32::consts::FRAC_PI_2)
314            * na::Rotation3::<f32>::from_axis_angle(&na::Vector3::x_axis(), std::f32::consts::FRAC_PI_2); //rotate 90 degrees
315        m
316    };
317
318    if transform_cpu_data {
319        //transform directly the verts
320        for mut vert in verts.row_iter_mut() {
321            let v_modif = model_matrix * na::Point3::from(vert.fixed_columns::<3>(0).transpose());
322            vert.copy_from_slice(v_modif.coords.as_slice());
323        }
324        //reset to identity
325        model_matrix = na::SimilarityMatrix3::<f32>::identity();
326    }
327    let verts_tensor = DynamicTensorFloat2D::from_dmatrix(&verts);
328    let edges_tensor = DynamicTensorInt2D::from_dmatrix(&edges);
329
330    let mut builder = EntityBuilder::new();
331    builder.add(Verts(verts_tensor)).add(Edges(edges_tensor)).add(ModelMatrix(model_matrix));
332
333    builder
334}
335
336pub fn build_from_file(path: &str) -> EntityBuilder {
337    //get filetype
338    let filetype = match Path::new(path).extension() {
339        Some(extension) => FileType::find_match(extension.to_str().unwrap_or("")),
340        None => FileType::Unknown,
341    };
342
343    #[allow(clippy::single_match_else)]
344    match filetype {
345        FileType::Obj => build_from_obj(Path::new(path)),
346        FileType::Ply => build_from_ply(Path::new(path)),
347        FileType::Gltf => build_from_gltf(Path::new(path)),
348        FileType::Unknown => {
349            error!("Could not read file {path:?}");
350            EntityBuilder::new() //empty builder
351        }
352    }
353}
354
355/// # Panics
356/// Will panic if the path cannot be opened
357#[cfg(target_arch = "wasm32")]
358pub async fn build_from_file_async(path: &str) -> EntityBuilder {
359    //get filetype
360    let filetype = match Path::new(path).extension() {
361        Some(extension) => FileType::find_match(extension.to_str().unwrap_or("")),
362        _ => FileType::Unknown,
363    };
364
365    match filetype {
366        FileType::Obj => build_from_obj_async(Path::new(path)).await,
367        // FileType::Ply => (),
368        _ => {
369            error!("Could not read file {:?}", path);
370            EntityBuilder::new() //empty builder
371        }
372    }
373}
374
375/// # Panics
376/// Will panic if the path cannot be opened
377#[allow(clippy::unused_async)] //uses async for wasm
378#[allow(clippy::identity_op)] //identity ops makes some things more explixit
379fn build_from_obj(path: &Path) -> EntityBuilder {
380    info!("reading obj from {path:?}");
381
382    //Native read
383    let (models, _) = tobj::load_obj(path, &tobj::GPU_LOAD_OPTIONS).expect("Failed to OBJ load file");
384
385    model_obj_to_entity_builder(&models)
386}
387
388/// # Panics
389/// Will panic if the path cannot be opened
390#[allow(clippy::unused_async)] //uses async for wasm
391#[cfg(target_arch = "wasm32")]
392#[allow(deprecated)]
393async fn build_from_obj_async(path: &Path) -> EntityBuilder {
394    //WASM read
395    let mut file_wasm = gloss_utils::io::FileLoader::open(path.to_str().unwrap()).await;
396    let (models, _) = tobj::load_obj_buf_async(&mut file_wasm, &tobj::GPU_LOAD_OPTIONS, move |p| async move {
397        match p.as_str() {
398            _ => unreachable!(),
399        }
400    })
401    .await
402    .expect("Failed to OBJ load file");
403
404    model_obj_to_entity_builder(&models)
405}
406
407/// # Panics
408/// Will panic if the path cannot be opened
409#[allow(clippy::unused_async)] //uses async for wasm
410#[allow(clippy::identity_op)] //identity ops makes some things more explixit
411pub fn build_from_obj_buf(buf: &[u8]) -> EntityBuilder {
412    let mut reader = std::io::BufReader::new(buf);
413
414    //Native read
415    let (models, _) = tobj::load_obj_buf(&mut reader, &tobj::GPU_LOAD_OPTIONS, move |_p| Err(tobj::LoadError::MaterialParseError))
416        .expect("Failed to OBJ load file");
417
418    model_obj_to_entity_builder(&models)
419}
420
421#[allow(clippy::identity_op)] //identity ops makes some things more explixit
422fn model_obj_to_entity_builder(models: &[tobj::Model]) -> EntityBuilder {
423    // fn model_obj_to_entity_builder(model: &ObjData) -> EntityBuilder{
424
425    let mesh = &models[0].mesh;
426    debug!("obj: nr indices {}", mesh.indices.len() / 3);
427    debug!("obj: nr positions {}", mesh.positions.len() / 3);
428    debug!("obj: nr normals {}", mesh.normals.len() / 3);
429    debug!("obj: nr texcoords {}", mesh.texcoords.len() / 2);
430
431    let nr_verts = mesh.positions.len() / 3;
432    let nr_faces = mesh.indices.len() / 3;
433    let nr_normals = mesh.normals.len() / 3;
434    let nr_texcoords = mesh.texcoords.len() / 2;
435
436    let mut builder = EntityBuilder::new();
437
438    if nr_verts > 0 {
439        debug!("read_obj: file has verts");
440        let verts = DMatrix::<f32>::from_row_slice(nr_verts, 3, mesh.positions.as_slice());
441        let verts_tensor = DynamicTensorFloat2D::from_dmatrix(&verts);
442        builder.add(Verts(verts_tensor));
443    }
444
445    if nr_faces > 0 {
446        debug!("read_obj: file has faces");
447        let faces = DMatrix::<u32>::from_row_slice(nr_faces, 3, mesh.indices.as_slice());
448        let faces_tensor = DynamicTensorInt2D::from_dmatrix(&faces);
449        builder.add(Faces(faces_tensor));
450    }
451
452    if nr_normals > 0 {
453        debug!("read_obj: file has normals");
454        let normals = DMatrix::<f32>::from_row_slice(nr_normals, 3, mesh.normals.as_slice());
455        let normals_tensor = DynamicTensorFloat2D::from_dmatrix(&normals);
456        builder.add(Normals(normals_tensor));
457    }
458
459    if nr_texcoords > 0 {
460        debug!("read_obj: file has texcoords");
461        let uv = DMatrix::<f32>::from_row_slice(nr_texcoords, 2, mesh.texcoords.as_slice());
462        let uvs_tensor = DynamicTensorFloat2D::from_dmatrix(&uv);
463        builder.add(UVs(uvs_tensor));
464    }
465
466    // if !mesh.faces_original_index.is_empty() {
467    //     builder.add(FacesOriginalIndex(mesh.faces_original_index.clone()));
468    // }
469
470    builder
471}
472
473/// # Panics
474/// Will panic if the path cannot be opened
475#[allow(clippy::unused_async)] //uses async for wasm
476#[allow(clippy::identity_op)] //identity ops makes some things more explixit
477#[allow(clippy::too_many_lines)] //identity ops makes some things more explixit
478fn build_from_ply(path: &Path) -> EntityBuilder {
479    #[derive(Debug, Default)]
480    pub struct Vertex {
481        pos: Vec3,
482        color: Vec3,
483        normal: Vec3,
484        uv: Vec2,
485    }
486
487    #[derive(Debug)]
488    pub struct Face {
489        vertex_index: Vec<u32>,
490    }
491
492    // The structs need to implement the PropertyAccess trait, otherwise the parser
493    // doesn't know how to write to them. Most functions have default, hence
494    // you only need to implement, what you expect to need.
495    impl ply::PropertyAccess for Vertex {
496        fn new() -> Self {
497            Self::default()
498        }
499        fn set_property(&mut self, key: String, property: ply::Property) {
500            match (key.as_ref(), property) {
501                ("x", ply::Property::Float(v)) => self.pos.x = v,
502                ("y", ply::Property::Float(v)) => self.pos.y = v,
503                ("z", ply::Property::Float(v)) => self.pos.z = v,
504                ("red", ply::Property::UChar(v)) => {
505                    self.color.x = f32::from(v) / 255.0;
506                }
507                ("green", ply::Property::UChar(v)) => self.color.y = f32::from(v) / 255.0,
508                ("blue", ply::Property::UChar(v)) => self.color.z = f32::from(v) / 255.0,
509                //normal
510                ("nx", ply::Property::Float(v)) => self.normal.x = v,
511                ("ny", ply::Property::Float(v)) => self.normal.y = v,
512                ("nz", ply::Property::Float(v)) => self.normal.z = v,
513                //uv
514                ("u" | "s", ply::Property::Float(v)) => self.uv.x = v,
515                ("v" | "t", ply::Property::Float(v)) => self.uv.y = v,
516                // (k, _) => panic!("Vertex: Unexpected key/value combination: key: {}", k),
517                // (k, prop) => {println!("unknown key {} of type {:?}", k, prop)},
518                (k, prop) => {
519                    warn!("unknown key {k} of type {prop:?}");
520                }
521            }
522        }
523    }
524
525    // same thing for Face
526    impl ply::PropertyAccess for Face {
527        fn new() -> Self {
528            Face { vertex_index: Vec::new() }
529        }
530        #[allow(clippy::cast_sign_loss)]
531        fn set_property(&mut self, key: String, property: ply::Property) {
532            match (key.as_ref(), property.clone()) {
533                ("vertex_indices" | "vertex_index", ply::Property::ListInt(vec)) => {
534                    self.vertex_index = vec.iter().map(|x| *x as u32).collect();
535                }
536                ("vertex_indices" | "vertex_index", ply::Property::ListUInt(vec)) => {
537                    self.vertex_index = vec;
538                }
539                (k, _) => {
540                    panic!("Face: Unexpected key/value combination: key, val: {k} {property:?}")
541                }
542            }
543        }
544    }
545
546    info!("reading ply from {path:?}");
547    // set up a reader, in this a file.
548    let f = std::fs::File::open(path).unwrap();
549    // The header of a ply file consists of ascii lines, BufRead provides useful
550    // methods for that.
551    let mut f = std::io::BufReader::new(f);
552
553    // Create a parser for each struct. Parsers are cheap objects.
554    let vertex_parser = parser::Parser::<Vertex>::new();
555    let face_parser = parser::Parser::<Face>::new();
556
557    // lets first consume the header
558    // We also could use `face_parser`, The configuration is a parser's only state.
559    // The reading position only depends on `f`.
560    let header = vertex_parser.read_header(&mut f).unwrap();
561
562    // Depending on the header, read the data into our structs..
563    let mut vertex_list = Vec::new();
564    let mut face_list = Vec::new();
565    for (_ignore_key, element) in &header.elements {
566        // we could also just parse them in sequence, but the file format might change
567        match element.name.as_ref() {
568            "vertex" | "point" => {
569                vertex_list = vertex_parser.read_payload_for_element(&mut f, element, &header).unwrap();
570            }
571            "face" => {
572                face_list = face_parser.read_payload_for_element(&mut f, element, &header).unwrap();
573            }
574            unknown_name => panic!("Unexpected element! {unknown_name}"),
575        }
576    }
577
578    let mut builder = EntityBuilder::new();
579
580    //pos
581    let mut verts = DMatrix::<f32>::zeros(vertex_list.len(), 3);
582    for (idx, v) in vertex_list.iter().enumerate() {
583        verts.row_mut(idx)[0] = v.pos.x;
584        verts.row_mut(idx)[1] = v.pos.y;
585        verts.row_mut(idx)[2] = v.pos.z;
586    }
587    let verts_tensor = DynamicTensorFloat2D::from_dmatrix(&verts);
588    builder.add(Verts(verts_tensor));
589    //color
590    let mut colors = DMatrix::<f32>::zeros(vertex_list.len(), 3);
591    for (idx, v) in vertex_list.iter().enumerate() {
592        colors.row_mut(idx)[0] = v.color.x;
593        colors.row_mut(idx)[1] = v.color.y;
594        colors.row_mut(idx)[2] = v.color.z;
595    }
596    //TODO need a better way to detect if we actually have color info
597    if colors.min() != 0.0 || colors.max() != 0.0 {
598        debug!("read_ply: file has colors");
599        let colors_tensor = DynamicTensorFloat2D::from_dmatrix(&colors);
600        builder.add(Colors(colors_tensor));
601    }
602    //normal
603    let mut normals = DMatrix::<f32>::zeros(vertex_list.len(), 3);
604    for (idx, v) in vertex_list.iter().enumerate() {
605        normals.row_mut(idx)[0] = v.normal.x;
606        normals.row_mut(idx)[1] = v.normal.y;
607        normals.row_mut(idx)[2] = v.normal.z;
608    }
609    //TODO need a better way to detect if we actually have normal info
610    if normals.min() != 0.0 || normals.max() != 0.0 {
611        debug!("read_ply: file has normals");
612        let normals_tensor = DynamicTensorFloat2D::from_dmatrix(&normals);
613        builder.add(Normals(normals_tensor));
614    }
615    //uv
616    let mut uvs = DMatrix::<f32>::zeros(vertex_list.len(), 2);
617    for (idx, v) in vertex_list.iter().enumerate() {
618        uvs.row_mut(idx)[0] = v.uv.x;
619        uvs.row_mut(idx)[1] = v.uv.y;
620    }
621    //TODO need a better way to detect if we actually have normal info
622    if uvs.min() != 0.0 || uvs.max() != 0.0 {
623        debug!("read_ply: file has uvs");
624        let uvs_tensor = DynamicTensorFloat2D::from_dmatrix(&uvs);
625        builder.add(UVs(uvs_tensor));
626    }
627
628    if !face_list.is_empty() {
629        debug!("read_ply: file has verts");
630        let mut faces = DMatrix::<u32>::zeros(face_list.len(), 3);
631        #[allow(clippy::cast_sign_loss)]
632        for (idx, f) in face_list.iter().enumerate() {
633            faces.row_mut(idx)[0] = f.vertex_index[0];
634            faces.row_mut(idx)[1] = f.vertex_index[1];
635            faces.row_mut(idx)[2] = f.vertex_index[2];
636        }
637        let faces_tensor = DynamicTensorInt2D::from_dmatrix(&faces);
638
639        builder.add(Faces(faces_tensor));
640    }
641
642    builder
643}
644
645/// # Panics
646/// Will panic if the path cannot be opened
647#[allow(clippy::too_many_lines)]
648fn build_from_gltf(path: &Path) -> EntityBuilder {
649    info!("reading gltf from {path:?}");
650
651    let (gltf, buffers, images) = gltf::import(path).expect("Failed to load GLTF file");
652
653    let mut builder = EntityBuilder::new();
654
655    // Accumulate data from all meshes and primitives
656    let mut all_positions = Vec::new();
657    let mut all_indices = Vec::new();
658    let mut all_normals = Vec::new();
659    let mut all_tex_coords = Vec::new();
660    let mut all_colors = Vec::new();
661
662    // Track textures to ensure we only have one
663    let mut found_texture: Option<usize> = None;
664    let mut has_multiple_textures = false;
665
666    //since we are merging multiple meshes, we need to offset the face indices
667    let mut vertex_offset = 0u32;
668
669    // Process all nodes to find mesh instances with their transformations
670    for scene in gltf.scenes() {
671        for node in scene.nodes() {
672            process_gltf_node(
673                &node,
674                &na::Matrix4::identity(),
675                &buffers,
676                &mut all_positions,
677                &mut all_indices,
678                &mut all_normals,
679                &mut all_tex_coords,
680                &mut all_colors,
681                &mut vertex_offset,
682                &mut found_texture,
683                &mut has_multiple_textures,
684            );
685        }
686    }
687
688    // Add accumulated data to builder
689    let nr_verts = all_positions.len() / 3;
690    if !all_positions.is_empty() {
691        debug!("gltf: total nr positions {nr_verts}");
692
693        let verts = DMatrix::<f32>::from_row_slice(nr_verts, 3, &all_positions);
694        let verts_tensor = DynamicTensorFloat2D::from_dmatrix(&verts);
695        builder.add(Verts(verts_tensor));
696    }
697
698    if !all_indices.is_empty() {
699        let nr_faces = all_indices.len() / 3;
700        debug!("gltf: total nr indices {}", all_indices.len());
701
702        let faces = DMatrix::<u32>::from_row_slice(nr_faces, 3, &all_indices);
703        let faces_tensor = DynamicTensorInt2D::from_dmatrix(&faces);
704        builder.add(Faces(faces_tensor));
705    }
706
707    if !all_normals.is_empty() {
708        let nr_normals = all_normals.len() / 3;
709        debug!("gltf: total nr normals {nr_normals}");
710
711        let normals_mat = DMatrix::<f32>::from_row_slice(nr_normals, 3, &all_normals);
712        let normals_tensor = DynamicTensorFloat2D::from_dmatrix(&normals_mat);
713        builder.add(Normals(normals_tensor));
714    }
715
716    if !all_tex_coords.is_empty() {
717        let nr_uvs = all_tex_coords.len() / 2;
718        debug!("gltf: total nr tex_coords {nr_uvs}");
719
720        let uvs = DMatrix::<f32>::from_row_slice(nr_uvs, 2, &all_tex_coords);
721        let uvs_tensor = DynamicTensorFloat2D::from_dmatrix(&uvs);
722        //if for some reason the number fo uvs does not match the number of vertices, we discard the uvs
723        if uvs.nrows() == nr_verts {
724            builder.add(UVs(uvs_tensor));
725        } else {
726            warn!(
727                "gltf: number of uvs {} does not match number of vertices {}, discarding uvs",
728                uvs.nrows(),
729                nr_verts
730            );
731        }
732    }
733
734    if !all_colors.is_empty() {
735        let nr_colors = all_colors.len() / 3;
736        debug!("gltf: total nr colors {nr_colors}");
737
738        let colors_mat = DMatrix::<f32>::from_row_slice(nr_colors, 3, &all_colors);
739        let colors_tensor = DynamicTensorFloat2D::from_dmatrix(&colors_mat);
740        builder.add(Colors(colors_tensor));
741    }
742
743    // Add texture if we found exactly one
744    if let Some(texture_index) = found_texture {
745        if !has_multiple_textures && texture_index < images.len() {
746            debug!("gltf: adding diffuse texture from image index {texture_index}");
747            let image_data = &images[texture_index];
748
749            // Convert the image data to the format expected by DiffuseImg
750            // Assuming DiffuseImg expects RGBA data
751            let img_data = match image_data.format {
752                gltf::image::Format::R8G8B8 => {
753                    // Convert RGB to RGBA by adding alpha channel
754                    let mut rgba_data = Vec::with_capacity(image_data.pixels.len() * 4 / 3);
755                    for chunk in image_data.pixels.chunks(3) {
756                        rgba_data.extend_from_slice(chunk);
757                        rgba_data.push(255); // Add full alpha
758                    }
759                    rgba_data
760                }
761                gltf::image::Format::R8G8B8A8 => image_data.pixels.clone(),
762                _ => {
763                    warn!("gltf: unsupported image format {:?}, skipping texture", image_data.format);
764                    Vec::new()
765                }
766            };
767
768            if !img_data.is_empty() {
769                let diffuse_img: DiffuseImg =
770                    DiffuseImg::new_from_raw_pixels(img_data, image_data.width, image_data.height, 4, &ImgConfig::default());
771                // don't flip the image but rather flip the uv coords
772                // diffuse_img.generic_img.cpu_img = Some(diffuse_img.generic_img.cpu_img.as_ref().unwrap().flipv());
773                builder.add(diffuse_img);
774            }
775        } else if has_multiple_textures {
776            warn!("gltf: multiple different textures found, discarding all textures");
777        }
778    }
779
780    builder
781}
782
783// Recursive function to process a GLTF node and its children, applying transformations and adding data to accumulator of position, indices, etc.
784#[allow(clippy::too_many_arguments)]
785fn process_gltf_node(
786    node: &gltf::Node,
787    parent_transform: &na::Matrix4<f32>,
788    buffers: &[gltf::buffer::Data],
789    all_positions: &mut Vec<f32>,
790    all_indices: &mut Vec<u32>,
791    all_normals: &mut Vec<f32>,
792    all_tex_coords: &mut Vec<f32>,
793    all_colors: &mut Vec<f32>,
794    vertex_offset: &mut u32,
795    found_texture: &mut Option<usize>,
796    has_multiple_textures: &mut bool,
797) {
798    // Get node transform - GLTF uses column-major matrices
799    let transform_array = node.transform().matrix();
800    let transform_slice: Vec<f32> = transform_array.iter().flatten().copied().collect();
801    let node_transform = na::Matrix4::from_column_slice(&transform_slice);
802    let combined_transform = parent_transform * node_transform;
803
804    // Process mesh if present
805    if let Some(mesh) = node.mesh() {
806        // info!("gltf: processing mesh {:?} with transform", mesh.name());
807
808        for primitive in mesh.primitives() {
809            let reader = primitive.reader(|buffer| Some(&buffers[buffer.index()]));
810
811            // Check for diffuse texture
812            if let Some(base_color_texture) = primitive.material().pbr_metallic_roughness().base_color_texture() {
813                let texture_index = base_color_texture.texture().source().index();
814
815                match found_texture {
816                    None => {
817                        *found_texture = Some(texture_index);
818                        debug!("gltf: found first texture at index {texture_index}");
819                    }
820                    Some(existing_index) => {
821                        if *existing_index != texture_index {
822                            *has_multiple_textures = true;
823                            debug!("gltf: found different texture at index {texture_index}, marking as multiple textures");
824                        }
825                    }
826                }
827            }
828
829            // Read and transform vertices
830            if let Some(positions) = reader.read_positions() {
831                let positions: Vec<[f32; 3]> = positions.collect();
832                for pos in positions {
833                    let point = na::Point3::new(pos[0], pos[1], pos[2]);
834                    let transformed = combined_transform.transform_point(&point);
835                    all_positions.extend_from_slice(transformed.coords.as_slice());
836                }
837            }
838
839            // Read faces/indices with reindexing
840            if let Some(indices) = reader.read_indices() {
841                let indices: Vec<u32> = indices.into_u32().map(|i| i + *vertex_offset).collect();
842                all_indices.extend(indices);
843            }
844
845            // Read and transform normals
846            if let Some(normals) = reader.read_normals() {
847                let normals: Vec<[f32; 3]> = normals.collect();
848                // For normals, we use the inverse transpose of the transformation matrix
849                // to correctly handle non-uniform scaling, For uniform scaling the inverse and then the transpose will be the same as the original matrix
850                let normal_transform = combined_transform.try_inverse().unwrap_or(na::Matrix4::identity()).transpose();
851                for normal in normals {
852                    let n = na::Vector3::new(normal[0], normal[1], normal[2]);
853                    let transformed = (normal_transform * n.to_homogeneous()).xyz().normalize();
854                    all_normals.extend_from_slice(transformed.as_slice());
855                }
856            }
857
858            // Read UV coordinates (no transformation needed)
859            if let Some(tex_coords) = reader.read_tex_coords(0) {
860                let mut tex_coords: Vec<f32> = tex_coords.into_f32().flatten().collect();
861                //flip up down
862                tex_coords.chunks_exact_mut(2).for_each(|chunk| {
863                    chunk[1] = 1.0 - chunk[1];
864                });
865                all_tex_coords.extend(tex_coords);
866            }
867
868            // Read vertex colors
869            if let Some(colors) = reader.read_colors(0) {
870                let colors: Vec<f32> = colors.into_rgb_f32().flatten().collect();
871                all_colors.extend(colors);
872            }
873
874            // Update vertex offset for next primitive
875            if let Some(positions) = reader.read_positions() {
876                *vertex_offset += u32::try_from(positions.count()).unwrap();
877            }
878        }
879    }
880
881    // Recursively process child nodes
882    for child in node.children() {
883        process_gltf_node(
884            &child,
885            &combined_transform,
886            buffers,
887            all_positions,
888            all_indices,
889            all_normals,
890            all_tex_coords,
891            all_colors,
892            vertex_offset,
893            found_texture,
894            has_multiple_textures,
895        );
896    }
897}