urdf_viz/
mesh.rs

1use crate::{errors::Result, utils::is_url};
2use k::nalgebra as na;
3use kiss3d::scene::SceneNode;
4use std::{cell::RefCell, ffi::OsStr, io, path::Path, rc::Rc};
5use tracing::*;
6
7#[cfg(feature = "assimp")]
8#[cfg(not(target_family = "wasm"))]
9fn load_mesh_assimp(
10    file_string: &str,
11    scale: na::Vector3<f32>,
12    opt_urdf_color: &Option<na::Point3<f32>>,
13    group: &mut SceneNode,
14    use_texture: bool,
15) -> Result<SceneNode> {
16    use crate::assimp_utils::*;
17
18    let filename = Path::new(file_string);
19
20    let mut base = group.add_group();
21    let mut importer = assimp::Importer::new();
22    importer.pre_transform_vertices(|x| x.enable = true);
23    importer.collada_ignore_up_direction(true);
24    importer.triangulate(true);
25    let (meshes, textures, colors) =
26        convert_assimp_scene_to_kiss3d_mesh(&importer.read_file(file_string)?);
27    info!(
28        "num mesh, texture, colors = {} {} {}",
29        meshes.len(),
30        textures.len(),
31        colors.len()
32    );
33    let mesh_scenes = meshes
34        .into_iter()
35        .map(|mesh| {
36            let mut scene = base.add_mesh(mesh, scale);
37            // use urdf color as default
38            if let Some(urdf_color) = *opt_urdf_color {
39                scene.set_color(urdf_color[0], urdf_color[1], urdf_color[2]);
40            }
41            scene
42        })
43        .collect::<Vec<_>>();
44    // use texture only for dae (collada)
45    let is_collada = matches!(
46        filename.extension().and_then(OsStr::to_str),
47        Some("dae" | "DAE")
48    );
49    // do not use texture, use only color in urdf file.
50    if !use_texture || !is_collada {
51        return Ok(base);
52    }
53
54    // Size of color and mesh are same, use each color for mesh
55    if mesh_scenes.len() == colors.len() {
56        for (count, (mut mesh_scene, color)) in
57            mesh_scenes.into_iter().zip(colors.into_iter()).enumerate()
58        {
59            mesh_scene.set_color(color[0], color[1], color[2]);
60            // Is this OK?
61            if count < textures.len() {
62                let mut texture_path = filename.to_path_buf();
63                texture_path.set_file_name(textures[count].clone());
64                debug!("using texture={}", texture_path.display());
65                if texture_path.exists() {
66                    mesh_scene.set_texture_from_file(&texture_path, texture_path.to_str().unwrap());
67                }
68            }
69        }
70    } else {
71        // When size of mesh and color mismatch, use only first color/texture for all meshes.
72        // If no color found, use urdf color instead.
73        for mut mesh_scene in mesh_scenes {
74            if !textures.is_empty() {
75                let mut texture_path = filename.to_path_buf();
76                texture_path.set_file_name(textures[0].clone());
77                debug!("texture={}", texture_path.display());
78                if texture_path.exists() {
79                    mesh_scene.set_texture_from_file(&texture_path, texture_path.to_str().unwrap());
80                }
81            }
82            if !colors.is_empty() {
83                let color = colors[0];
84                mesh_scene.set_color(color[0], color[1], color[2]);
85            }
86        }
87    }
88    Ok(base)
89}
90
91#[cfg(not(target_family = "wasm"))]
92pub fn load_mesh(
93    filename: impl AsRef<str>,
94    scale: na::Vector3<f32>,
95    opt_color: &Option<na::Point3<f32>>,
96    group: &mut SceneNode,
97    use_texture: bool,
98    #[allow(unused_variables)] use_assimp: bool,
99) -> Result<SceneNode> {
100    let file_string = filename.as_ref();
101
102    #[cfg(feature = "assimp")]
103    if use_assimp {
104        if is_url(file_string) {
105            let file = crate::utils::fetch_tempfile(file_string)?;
106            let path = file.path().to_str().unwrap();
107            return load_mesh_assimp(path, scale, opt_color, group, use_texture);
108        }
109        return load_mesh_assimp(file_string, scale, opt_color, group, use_texture);
110    }
111
112    let ext = Path::new(file_string).extension().and_then(OsStr::to_str);
113    debug!("load {ext:?}: path = {file_string}");
114    load_with_mesh_loader(
115        &fetch_or_read(file_string)?,
116        file_string,
117        scale,
118        opt_color,
119        group,
120        use_texture,
121    )
122}
123
124#[cfg(not(target_family = "wasm"))]
125fn fetch_or_read(filename: &str) -> Result<Vec<u8>> {
126    use std::io::Read;
127
128    const RESPONSE_SIZE_LIMIT: usize = 10 * 1_024 * 1_024;
129
130    if is_url(filename) {
131        let mut buf = Vec::with_capacity(128);
132        ureq::get(filename)
133            .call()
134            .map_err(|e| crate::Error::Other(e.to_string()))?
135            .into_reader()
136            .take((RESPONSE_SIZE_LIMIT + 1) as u64)
137            .read_to_end(&mut buf)?;
138        if buf.len() > RESPONSE_SIZE_LIMIT {
139            return Err(crate::errors::Error::Other(format!(
140                "{filename} is too big"
141            )));
142        }
143        Ok(buf)
144    } else {
145        Ok(std::fs::read(filename)?)
146    }
147}
148
149/// NOTE: Unlike other platforms, the first argument should be the data loaded
150/// by [`utils::load_mesh`](crate::utils::load_mesh), not the path.
151#[cfg(target_family = "wasm")]
152pub fn load_mesh(
153    data: impl AsRef<str>,
154    scale: na::Vector3<f32>,
155    opt_color: &Option<na::Point3<f32>>,
156    group: &mut SceneNode,
157    _use_texture: bool,
158    _use_assimp: bool,
159) -> Result<SceneNode> {
160    let data = crate::utils::Mesh::decode(data.as_ref())?;
161    let ext = Path::new(&data.path).extension().and_then(OsStr::to_str);
162    debug!("load {ext:?}: path = {}", data.path);
163    let use_texture = false;
164    load_with_mesh_loader(
165        data.bytes().unwrap(),
166        &data.path,
167        scale,
168        opt_color,
169        group,
170        use_texture,
171    )
172}
173
174fn load_with_mesh_loader(
175    bytes: &[u8],
176    file_string: &str,
177    scale: na::Vector3<f32>,
178    opt_color: &Option<na::Point3<f32>>,
179    group: &mut SceneNode,
180    mut use_texture: bool,
181) -> Result<SceneNode> {
182    let mut base = group.add_group();
183    let mut loader = mesh_loader::Loader::default();
184    use_texture &= !is_url(file_string);
185    if use_texture {
186        // TODO: Using fetch_or_read can support remote materials, but loading becomes slow.
187        // #[cfg(not(target_family = "wasm"))]
188        // {
189        //     loader = loader
190        //         .custom_reader(|p| fetch_or_read(p.to_str().unwrap()).map_err(io::Error::other));
191        // }
192    } else {
193        loader = loader.custom_reader(|_| Err(io::Error::other("texture rendering disabled")));
194    }
195    let scene = loader.load_from_slice(bytes, file_string).map_err(|e| {
196        if e.kind() == io::ErrorKind::Unsupported {
197            crate::errors::Error::from(format!(
198                "{file_string} is not supported, because assimp feature is disabled"
199            ))
200        } else {
201            e.into()
202        }
203    })?;
204
205    for (mesh, material) in scene.meshes.into_iter().zip(scene.materials) {
206        let coords = mesh.vertices.into_iter().map(Into::into).collect();
207        let faces = mesh
208            .faces
209            .into_iter()
210            // TODO: https://github.com/openrr/urdf-viz/issues/22
211            .map(|f| na::Point3::new(f[0] as u16, f[1] as u16, f[2] as u16))
212            .collect();
213
214        let kiss3d_mesh = Rc::new(RefCell::new(kiss3d::resource::Mesh::new(
215            coords, faces, None, None, false,
216        )));
217        let mut kiss3d_scene = base.add_mesh(kiss3d_mesh, scale);
218        if use_texture {
219            if let Some(color) = material.color.diffuse {
220                kiss3d_scene.set_color(color[0], color[1], color[2]);
221            }
222            if let Some(path) = &material.texture.diffuse {
223                let path_string = path.to_str().unwrap();
224                // TODO: Using fetch_or_read can support remote materials, but loading becomes slow.
225                // let buf = fetch_or_read(path_string)?;
226                // kiss3d_scene.set_texture_from_memory(&buf, path_string);
227                kiss3d_scene.set_texture_from_file(path, path_string);
228            }
229            if let Some(path) = &material.texture.ambient {
230                let path_string = path.to_str().unwrap();
231                // TODO: Using fetch_or_read can support remote materials, but loading becomes slow.
232                // let buf = fetch_or_read(path_string)?;
233                // kiss3d_scene.set_texture_from_memory(&buf, path_string);
234                kiss3d_scene.set_texture_from_file(path, path_string);
235            }
236        }
237    }
238    if let Some(color) = *opt_color {
239        base.set_color(color[0], color[1], color[2]);
240    }
241    Ok(base)
242}