From be2587c5f87adc9216d432352c3e3fb549b6712b Mon Sep 17 00:00:00 2001 From: Alexy Legrand Date: Mon, 25 Aug 2025 10:23:55 +0200 Subject: [PATCH 01/15] add solution to load collada --- .../pinocchio/visualize/viser_visualizer.py | 103 +++++++++++++++--- 1 file changed, 89 insertions(+), 14 deletions(-) diff --git a/bindings/python/pinocchio/visualize/viser_visualizer.py b/bindings/python/pinocchio/visualize/viser_visualizer.py index 603fc701c1..77f83354c4 100644 --- a/bindings/python/pinocchio/visualize/viser_visualizer.py +++ b/bindings/python/pinocchio/visualize/viser_visualizer.py @@ -1,4 +1,5 @@ import time +import os try: import hppfcl @@ -7,12 +8,15 @@ import numpy as np +from trimesh.exchange import dae from .. import pinocchio_pywrap_default as pin from . import BaseVisualizer try: import trimesh # Required by viser import viser + import collada + except ImportError: import_viser_succeed = False else: @@ -214,21 +218,92 @@ def loadViewerGeometryObject(self, geometry_object, prefix="", color=None): self.frames[name] = frame - def _add_mesh_from_path(self, name, mesh_path, color): - """ - Load a mesh from a file. - """ - mesh = trimesh.load_mesh(mesh_path) + def _add_mesh_from_path(self, name, mesh_path, color=None): + + ext = os.path.splitext(mesh_path)[1].lower() + extracted_color = None + + if ext == ".dae": + try: + # Load the .dae file using pycollada + mesh_collada = collada.Collada(mesh_path) + + # Method 1: Via pycollada + for effect in mesh_collada.effects: + if hasattr(effect, 'diffuse') and effect.diffuse is not None: + # Check if it's a Color object (not a texture) + if hasattr(effect.diffuse, 'color') and effect.diffuse.color is not None: + color_values = effect.diffuse.color + if len(color_values) == 3: + extracted_color = list(color_values) + [1.0] + elif len(color_values) == 4: + extracted_color = list(color_values) + print(f"[INFO] Extracted color from DAE: {extracted_color}") + break + # Alternative: direct access if it's a color array + elif hasattr(effect.diffuse, '__iter__') and len(effect.diffuse) >= 3: + color_values = effect.diffuse + if len(color_values) == 3: + extracted_color = list(color_values) + [1.0] + else: + extracted_color = list(color_values) + print(f"[INFO] Extracted color from DAE (direct): {extracted_color}") + break + + # Method 2: If pycollada didn't work, parse XML directly + if extracted_color is None: + import xml.etree.ElementTree as ET + tree = ET.parse(mesh_path) + root = tree.getroot() + + # Find diffuse tags with color + for diffuse in root.iter(): + if diffuse.tag.endswith('diffuse'): + color_elem = diffuse.find('.//color') + if color_elem is not None and color_elem.text: + color_text = color_elem.text.strip().split() + if len(color_text) >= 3: + try: + color_values = [float(x) for x in color_text[:4]] + if len(color_values) == 3: + extracted_color = color_values + [1.0] + else: + extracted_color = color_values + print(f"[INFO] Extracted color from DAE via XML: {extracted_color}") + break + except ValueError: + continue + + except Exception as e: + print(f"[ERROR] Failed to extract color from DAE {mesh_path} : {e}") + extracted_color = [0.9, 0.9, 0.9, 1.0] # fallback + + # Use the extracted color if no color was provided as parameter if color is None: - return self.viewer.scene.add_mesh_trimesh(name, mesh) - else: - return self.viewer.scene.add_mesh_simple( - name, - mesh.vertices, - mesh.faces, - color=color[:3], - opacity=color[3], - ) + color = extracted_color + + # General case for all formats + try: + mesh = trimesh.load(mesh_path, force='mesh') + + if color is None: + # No color specified, use the default mesh + return self.viewer.scene.add_mesh_trimesh(name, mesh) + else: + color = [.25,.25,.25,1.] + # Specified color (extracted or provided) + return self.viewer.scene.add_mesh_simple( + name, + mesh.vertices, + mesh.faces, + color=color[:3], + opacity=color[3] + ) + + except Exception as e: + print(f"[ERROR] Failed to create mesh {mesh_path} : {e}") + return None + def _add_mesh_from_convex(self, name, geom, color): """ From f7609067fd54b1179394f2ac5c17a934105246fc Mon Sep 17 00:00:00 2001 From: Alexy Legrand Date: Mon, 25 Aug 2025 10:30:08 +0200 Subject: [PATCH 02/15] add solution to load collada --- bindings/python/pinocchio/visualize/viser_visualizer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bindings/python/pinocchio/visualize/viser_visualizer.py b/bindings/python/pinocchio/visualize/viser_visualizer.py index 77f83354c4..03282ea3b9 100644 --- a/bindings/python/pinocchio/visualize/viser_visualizer.py +++ b/bindings/python/pinocchio/visualize/viser_visualizer.py @@ -290,7 +290,7 @@ def _add_mesh_from_path(self, name, mesh_path, color=None): # No color specified, use the default mesh return self.viewer.scene.add_mesh_trimesh(name, mesh) else: - color = [.25,.25,.25,1.] + color = [.25,.25,.20,1.] # Specified color (extracted or provided) return self.viewer.scene.add_mesh_simple( name, From 2347877eb0e6fe92ebb611fa394e00943c8f52d2 Mon Sep 17 00:00:00 2001 From: Alexy Legrand Date: Tue, 2 Sep 2025 09:14:42 +0200 Subject: [PATCH 03/15] fix default color handling in viser_visualizer.py --- .../pinocchio/visualize/viser_visualizer.py | 121 ++++++------------ 1 file changed, 36 insertions(+), 85 deletions(-) diff --git a/bindings/python/pinocchio/visualize/viser_visualizer.py b/bindings/python/pinocchio/visualize/viser_visualizer.py index 03282ea3b9..2ce232922b 100644 --- a/bindings/python/pinocchio/visualize/viser_visualizer.py +++ b/bindings/python/pinocchio/visualize/viser_visualizer.py @@ -218,92 +218,43 @@ def loadViewerGeometryObject(self, geometry_object, prefix="", color=None): self.frames[name] = frame - def _add_mesh_from_path(self, name, mesh_path, color=None): - - ext = os.path.splitext(mesh_path)[1].lower() - extracted_color = None - - if ext == ".dae": - try: - # Load the .dae file using pycollada - mesh_collada = collada.Collada(mesh_path) - - # Method 1: Via pycollada - for effect in mesh_collada.effects: - if hasattr(effect, 'diffuse') and effect.diffuse is not None: - # Check if it's a Color object (not a texture) - if hasattr(effect.diffuse, 'color') and effect.diffuse.color is not None: - color_values = effect.diffuse.color - if len(color_values) == 3: - extracted_color = list(color_values) + [1.0] - elif len(color_values) == 4: - extracted_color = list(color_values) - print(f"[INFO] Extracted color from DAE: {extracted_color}") - break - # Alternative: direct access if it's a color array - elif hasattr(effect.diffuse, '__iter__') and len(effect.diffuse) >= 3: - color_values = effect.diffuse - if len(color_values) == 3: - extracted_color = list(color_values) + [1.0] - else: - extracted_color = list(color_values) - print(f"[INFO] Extracted color from DAE (direct): {extracted_color}") - break - - # Method 2: If pycollada didn't work, parse XML directly - if extracted_color is None: - import xml.etree.ElementTree as ET - tree = ET.parse(mesh_path) - root = tree.getroot() - - # Find diffuse tags with color - for diffuse in root.iter(): - if diffuse.tag.endswith('diffuse'): - color_elem = diffuse.find('.//color') - if color_elem is not None and color_elem.text: - color_text = color_elem.text.strip().split() - if len(color_text) >= 3: - try: - color_values = [float(x) for x in color_text[:4]] - if len(color_values) == 3: - extracted_color = color_values + [1.0] - else: - extracted_color = color_values - print(f"[INFO] Extracted color from DAE via XML: {extracted_color}") - break - except ValueError: - continue - - except Exception as e: - print(f"[ERROR] Failed to extract color from DAE {mesh_path} : {e}") - extracted_color = [0.9, 0.9, 0.9, 1.0] # fallback - - # Use the extracted color if no color was provided as parameter - if color is None: - color = extracted_color - - # General case for all formats - try: - mesh = trimesh.load(mesh_path, force='mesh') - - if color is None: - # No color specified, use the default mesh - return self.viewer.scene.add_mesh_trimesh(name, mesh) - else: - color = [.25,.25,.20,1.] - # Specified color (extracted or provided) - return self.viewer.scene.add_mesh_simple( - name, - mesh.vertices, - mesh.faces, - color=color[:3], - opacity=color[3] - ) - - except Exception as e: - print(f"[ERROR] Failed to create mesh {mesh_path} : {e}") - return None + def _add_mesh_from_path(self, name, mesh_path, color=None): + """ + Load a mesh from a file and add it to the scene. + """ + mesh_collada = collada.Collada(mesh_path) + for i, (geometry, effect) in enumerate( + zip(mesh_collada.geometries, mesh_collada.effects) + ): + try: + # extraction des données + vertices = geometry.primitives[0].sources["VERTEX"][0][4].data + indices = geometry.primitives[0].indices + + # indices 3D ou 2D + if indices.ndim == 3: + faces = indices[:, :, 0] + else: + faces = indices + + # gestion de la couleur + color = getattr(effect, "diffuse", None) + + if color is None: + mesh = trimesh.load_mesh(mesh_path) + self.viewer.scene.add_mesh_trimesh(f"{name}_{i}", mesh) + else: + self.viewer.scene.add_mesh_simple( + f"{name}_{i}", + vertices, + faces, + color=color[:3], + opacity=color[3], + ) + except Exception: + mesh = trimesh.load_mesh(mesh_path) + self.viewer.scene.add_mesh_trimesh(f"{name}_{i}", mesh) def _add_mesh_from_convex(self, name, geom, color): """ From ea797d480246bd662d4d875edf53650c3c1eceac Mon Sep 17 00:00:00 2001 From: Alexy Legrand Date: Mon, 22 Sep 2025 09:16:39 +0200 Subject: [PATCH 04/15] change the default robot in viser-viewer.py to test --- examples/gepetto-viewer.py | 4 ++-- examples/viser-viewer.py | 4 ++-- leap.mp4 | Bin 0 -> 8 bytes 3 files changed, 4 insertions(+), 4 deletions(-) create mode 100644 leap.mp4 diff --git a/examples/gepetto-viewer.py b/examples/gepetto-viewer.py index 95faf6ed3b..b996b9b946 100644 --- a/examples/gepetto-viewer.py +++ b/examples/gepetto-viewer.py @@ -11,8 +11,8 @@ # Load the URDF model. model_path = Path(os.environ.get("EXAMPLE_ROBOT_DATA_MODEL_DIR")) mesh_dir = model_path.parent.parent -urdf_filename = "talos_reduced.urdf" -urdf_model_path = model_path / "talos_data/robots" / urdf_filename +urdf_filename = "panda.urdf" +urdf_model_path = model_path / "panda_description/urdf" / urdf_filename model, collision_model, visual_model = pin.buildModelsFromUrdf( urdf_model_path, mesh_dir, pin.JointModelFreeFlyer() diff --git a/examples/viser-viewer.py b/examples/viser-viewer.py index dd4fbe20e1..33971c6470 100644 --- a/examples/viser-viewer.py +++ b/examples/viser-viewer.py @@ -17,8 +17,8 @@ mesh_dir = pinocchio_model_dir # urdf_filename = "talos_reduced.urdf" # urdf_model_path = join(join(model_path,"talos_data/robots"),urdf_filename) -urdf_filename = "solo.urdf" -urdf_model_path = model_path / "solo_description/robots" / urdf_filename +urdf_filename = "tiago.urdf" +urdf_model_path = model_path / "tiago_description/robots" / urdf_filename model, collision_model, visual_model = pin.buildModelsFromUrdf( urdf_model_path, mesh_dir, pin.JointModelFreeFlyer() diff --git a/leap.mp4 b/leap.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..997c69fe8f92889a927c5ad5de83611677d727a9 GIT binary patch literal 8 McmebD)M5Yu00m0`y#N3J literal 0 HcmV?d00001 From 68c539c95649961c2206f4a421db7668d140a66b Mon Sep 17 00:00:00 2001 From: Alexy Legrand Date: Tue, 30 Sep 2025 09:56:07 +0200 Subject: [PATCH 05/15] change the function add_mesh_from_path to load color with collada --- .../pinocchio/visualize/viser_visualizer.py | 84 ++++++++++++------- 1 file changed, 52 insertions(+), 32 deletions(-) diff --git a/bindings/python/pinocchio/visualize/viser_visualizer.py b/bindings/python/pinocchio/visualize/viser_visualizer.py index 2ce232922b..6f1c0f20dd 100644 --- a/bindings/python/pinocchio/visualize/viser_visualizer.py +++ b/bindings/python/pinocchio/visualize/viser_visualizer.py @@ -218,43 +218,63 @@ def loadViewerGeometryObject(self, geometry_object, prefix="", color=None): self.frames[name] = frame - def _add_mesh_from_path(self, name, mesh_path, color=None): - """ - Load a mesh from a file and add it to the scene. - """ - mesh_collada = collada.Collada(mesh_path) - - for i, (geometry, effect) in enumerate( - zip(mesh_collada.geometries, mesh_collada.effects) - ): - try: - # extraction des données - vertices = geometry.primitives[0].sources["VERTEX"][0][4].data - indices = geometry.primitives[0].indices + def _add_mesh_from_path(self, name, mesh_path, color=None): + ext = os.path.splitext(mesh_path)[1].lower() + print(f"[DEBUG] _add_mesh_from_path: {mesh_path} (ext={ext})") - # indices 3D ou 2D - if indices.ndim == 3: - faces = indices[:, :, 0] - else: - faces = indices + if ext not in [".dae", ".stl", ".obj"]: + print(f"[WARNING] Ignoring non-mesh file: {mesh_path}") + return - # gestion de la couleur - color = getattr(effect, "diffuse", None) + if ext == ".dae": + try: + mesh_collada = collada.Collada(mesh_path) - if color is None: + if len(mesh_collada.effects) < len(mesh_collada.geometries): mesh = trimesh.load_mesh(mesh_path) - self.viewer.scene.add_mesh_trimesh(f"{name}_{i}", mesh) - else: - self.viewer.scene.add_mesh_simple( - f"{name}_{i}", - vertices, - faces, - color=color[:3], - opacity=color[3], - ) - except Exception: + self.viewer.scene.add_mesh_trimesh(f"{name}", mesh) + return + + for i, (geometry, effect) in enumerate( + zip(mesh_collada.geometries, mesh_collada.effects) + ): + try: + vertices = geometry.primitives[0].sources["VERTEX"][0][4].data + indices = geometry.primitives[0].indices + + if indices.ndim == 3: + faces = indices[:, :, 0] + else: + faces = indices + + color = getattr(effect, "diffuse", None) + + if color is None: + mesh = trimesh.load_mesh(mesh_path) + self.viewer.scene.add_mesh_trimesh(f"{name}_{i}", mesh) + else: + self.viewer.scene.add_mesh_simple( + f"{name}_{i}", + vertices, + faces, + color=color[:3], + opacity=color[3], + ) + + except Exception as e: + print(f"[ERROR] Collada parsing failed: {e}") + mesh = trimesh.load_mesh(mesh_path) + self.viewer.scene.add_mesh_trimesh(f"{name}_{i}", mesh) + + except Exception as e: + print(f"[ERROR] Failed to load {mesh_path} as .dae: {e}") mesh = trimesh.load_mesh(mesh_path) - self.viewer.scene.add_mesh_trimesh(f"{name}_{i}", mesh) + self.viewer.scene.add_mesh_trimesh(f"{name}", mesh) + + else: + # Cas STL / OBJ → directement avec trimesh + mesh = trimesh.load_mesh(mesh_path) + self.viewer.scene.add_mesh_trimesh(f"{name}", mesh) def _add_mesh_from_convex(self, name, geom, color): """ From ac14f5f6556ea27b753b74d8872aad2673c62f54 Mon Sep 17 00:00:00 2001 From: Alexy Legrand Date: Tue, 7 Oct 2025 15:12:42 +0200 Subject: [PATCH 06/15] Ensure all geometry types are correctly handled to prevent NoneType errors. --- bindings/python/pinocchio/visualize/viser_visualizer.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/bindings/python/pinocchio/visualize/viser_visualizer.py b/bindings/python/pinocchio/visualize/viser_visualizer.py index 6f1c0f20dd..ca9104bfc6 100644 --- a/bindings/python/pinocchio/visualize/viser_visualizer.py +++ b/bindings/python/pinocchio/visualize/viser_visualizer.py @@ -220,8 +220,7 @@ def loadViewerGeometryObject(self, geometry_object, prefix="", color=None): def _add_mesh_from_path(self, name, mesh_path, color=None): ext = os.path.splitext(mesh_path)[1].lower() - print(f"[DEBUG] _add_mesh_from_path: {mesh_path} (ext={ext})") - + if ext not in [".dae", ".stl", ".obj"]: print(f"[WARNING] Ignoring non-mesh file: {mesh_path}") return @@ -272,7 +271,6 @@ def _add_mesh_from_path(self, name, mesh_path, color=None): self.viewer.scene.add_mesh_trimesh(f"{name}", mesh) else: - # Cas STL / OBJ → directement avec trimesh mesh = trimesh.load_mesh(mesh_path) self.viewer.scene.add_mesh_trimesh(f"{name}", mesh) From 9decb6c1b71fc592c22febb4b2d846a653462e8c Mon Sep 17 00:00:00 2001 From: Alexy Legrand Date: Tue, 7 Oct 2025 15:24:36 +0200 Subject: [PATCH 07/15] Ensure all geometry types are correctly handled to prevent NoneType errors. --- bindings/python/pinocchio/visualize/viser_visualizer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bindings/python/pinocchio/visualize/viser_visualizer.py b/bindings/python/pinocchio/visualize/viser_visualizer.py index ca9104bfc6..eec9c32dd0 100644 --- a/bindings/python/pinocchio/visualize/viser_visualizer.py +++ b/bindings/python/pinocchio/visualize/viser_visualizer.py @@ -220,7 +220,7 @@ def loadViewerGeometryObject(self, geometry_object, prefix="", color=None): def _add_mesh_from_path(self, name, mesh_path, color=None): ext = os.path.splitext(mesh_path)[1].lower() - + if ext not in [".dae", ".stl", ".obj"]: print(f"[WARNING] Ignoring non-mesh file: {mesh_path}") return From 7e2e7a929f5a775044e0046f664ef3f62eb72396 Mon Sep 17 00:00:00 2001 From: Alexy Legrand Date: Fri, 10 Oct 2025 09:54:50 +0200 Subject: [PATCH 08/15] handle incorrect faces array shape in COLLADA mesh loading --- .../pinocchio/visualize/viser_visualizer.py | 68 ++++++++++++------- 1 file changed, 44 insertions(+), 24 deletions(-) diff --git a/bindings/python/pinocchio/visualize/viser_visualizer.py b/bindings/python/pinocchio/visualize/viser_visualizer.py index eec9c32dd0..a0c81b410a 100644 --- a/bindings/python/pinocchio/visualize/viser_visualizer.py +++ b/bindings/python/pinocchio/visualize/viser_visualizer.py @@ -219,60 +219,80 @@ def loadViewerGeometryObject(self, geometry_object, prefix="", color=None): self.frames[name] = frame def _add_mesh_from_path(self, name, mesh_path, color=None): + """ + Load a mesh from a file + """ ext = os.path.splitext(mesh_path)[1].lower() - - if ext not in [".dae", ".stl", ".obj"]: - print(f"[WARNING] Ignoring non-mesh file: {mesh_path}") - return - if ext == ".dae": try: mesh_collada = collada.Collada(mesh_path) - if len(mesh_collada.effects) < len(mesh_collada.geometries): mesh = trimesh.load_mesh(mesh_path) - self.viewer.scene.add_mesh_trimesh(f"{name}", mesh) - return - + return self.viewer.scene.add_mesh_trimesh(f"{name}", mesh) + + frames = [] for i, (geometry, effect) in enumerate( zip(mesh_collada.geometries, mesh_collada.effects) ): try: vertices = geometry.primitives[0].sources["VERTEX"][0][4].data indices = geometry.primitives[0].indices - + if indices.ndim == 3: faces = indices[:, :, 0] - else: + elif indices.ndim == 2: faces = indices - + elif indices.ndim == 1: + faces = indices.reshape(-1, 3) + else: + raise ValueError(f"indices has an unexpected form: {indices.shape}") + + if faces.shape[-1] != 3: + if faces.size % 3 == 0: + faces = faces.reshape(-1, 3) + else: + raise ValueError(f"Unable to reshape faces in (N, 3): {faces.shape}") + color = getattr(effect, "diffuse", None) - if color is None: mesh = trimesh.load_mesh(mesh_path) - self.viewer.scene.add_mesh_trimesh(f"{name}_{i}", mesh) + frame = self.viewer.scene.add_mesh_trimesh(f"{name}_{i}", mesh) else: - self.viewer.scene.add_mesh_simple( + frame = self.viewer.scene.add_mesh_simple( f"{name}_{i}", vertices, faces, color=color[:3], opacity=color[3], ) - + + if frame is None: + frame = self.viewer.scene.add_box( + f"{name}_{i}_placeholder", + (0.001, 0.001, 0.001), + color=(200, 200, 200) + ) + frames.append(frame) + except Exception as e: - print(f"[ERROR] Collada parsing failed: {e}") - mesh = trimesh.load_mesh(mesh_path) - self.viewer.scene.add_mesh_trimesh(f"{name}_{i}", mesh) - + import traceback + try: + mesh = trimesh.load_mesh(mesh_path) + frame = self.viewer.scene.add_mesh_trimesh(f"{name}_{i}", mesh) + if frame is not None: + frames.append(frame) + except Exception as e2: + pass + + return frames[0] if frames else None + except Exception as e: - print(f"[ERROR] Failed to load {mesh_path} as .dae: {e}") + import traceback mesh = trimesh.load_mesh(mesh_path) - self.viewer.scene.add_mesh_trimesh(f"{name}", mesh) - + return self.viewer.scene.add_mesh_trimesh(f"{name}", mesh) else: mesh = trimesh.load_mesh(mesh_path) - self.viewer.scene.add_mesh_trimesh(f"{name}", mesh) + return self.viewer.scene.add_mesh_trimesh(f"{name}", mesh) def _add_mesh_from_convex(self, name, geom, color): """ From 16c3bed1de7b7e3c1b652a37f2acd4ef606985e0 Mon Sep 17 00:00:00 2001 From: Alexy Legrand Date: Tue, 14 Oct 2025 10:26:04 +0200 Subject: [PATCH 09/15] improved handling of geometry indices (added explicit checks and reshaping) --- .../pinocchio/visualize/viser_visualizer.py | 62 +++++++++++++------ 1 file changed, 44 insertions(+), 18 deletions(-) diff --git a/bindings/python/pinocchio/visualize/viser_visualizer.py b/bindings/python/pinocchio/visualize/viser_visualizer.py index a0c81b410a..a60b2b662a 100644 --- a/bindings/python/pinocchio/visualize/viser_visualizer.py +++ b/bindings/python/pinocchio/visualize/viser_visualizer.py @@ -220,15 +220,23 @@ def loadViewerGeometryObject(self, geometry_object, prefix="", color=None): def _add_mesh_from_path(self, name, mesh_path, color=None): """ - Load a mesh from a file + Load a mesh from a file. """ ext = os.path.splitext(mesh_path)[1].lower() + if ext == ".dae": try: mesh_collada = collada.Collada(mesh_path) + if len(mesh_collada.effects) < len(mesh_collada.geometries): mesh = trimesh.load_mesh(mesh_path) - return self.viewer.scene.add_mesh_trimesh(f"{name}", mesh) + if color is None: + return self.viewer.scene.add_mesh_trimesh(name, mesh) + else: + return self.viewer.scene.add_mesh_simple( + name, mesh.vertices, mesh.faces, + color=color[:3], opacity=color[3] + ) frames = [] for i, (geometry, effect) in enumerate( @@ -245,19 +253,23 @@ def _add_mesh_from_path(self, name, mesh_path, color=None): elif indices.ndim == 1: faces = indices.reshape(-1, 3) else: - raise ValueError(f"indices has an unexpected form: {indices.shape}") + faces = indices.reshape(-1, 3) if faces.shape[-1] != 3: if faces.size % 3 == 0: faces = faces.reshape(-1, 3) - else: - raise ValueError(f"Unable to reshape faces in (N, 3): {faces.shape}") - color = getattr(effect, "diffuse", None) - if color is None: - mesh = trimesh.load_mesh(mesh_path) - frame = self.viewer.scene.add_mesh_trimesh(f"{name}_{i}", mesh) - else: + mesh_color = getattr(effect, "diffuse", None) + + if mesh_color is not None: + frame = self.viewer.scene.add_mesh_simple( + f"{name}_{i}", + vertices, + faces, + color=mesh_color[:3], + opacity=mesh_color[3], + ) + elif color is not None: frame = self.viewer.scene.add_mesh_simple( f"{name}_{i}", vertices, @@ -265,6 +277,9 @@ def _add_mesh_from_path(self, name, mesh_path, color=None): color=color[:3], opacity=color[3], ) + else: + mesh = trimesh.load_mesh(mesh_path) + frame = self.viewer.scene.add_mesh_trimesh(f"{name}_{i}", mesh) if frame is None: frame = self.viewer.scene.add_box( @@ -272,27 +287,38 @@ def _add_mesh_from_path(self, name, mesh_path, color=None): (0.001, 0.001, 0.001), color=(200, 200, 200) ) + frames.append(frame) - except Exception as e: - import traceback + except Exception: try: mesh = trimesh.load_mesh(mesh_path) frame = self.viewer.scene.add_mesh_trimesh(f"{name}_{i}", mesh) if frame is not None: frames.append(frame) - except Exception as e2: + except Exception: pass return frames[0] if frames else None - except Exception as e: - import traceback + except Exception: mesh = trimesh.load_mesh(mesh_path) - return self.viewer.scene.add_mesh_trimesh(f"{name}", mesh) + if color is None: + return self.viewer.scene.add_mesh_trimesh(name, mesh) + else: + return self.viewer.scene.add_mesh_simple( + name, mesh.vertices, mesh.faces, + color=color[:3], opacity=color[3] + ) + + mesh = trimesh.load_mesh(mesh_path) + if color is None: + return self.viewer.scene.add_mesh_trimesh(name, mesh) else: - mesh = trimesh.load_mesh(mesh_path) - return self.viewer.scene.add_mesh_trimesh(f"{name}", mesh) + return self.viewer.scene.add_mesh_simple( + name, mesh.vertices, mesh.faces, + color=color[:3], opacity=color[3] + ) def _add_mesh_from_convex(self, name, geom, color): """ From e0bfdf164af6430cb374e356a3655f6db59e9e27 Mon Sep 17 00:00:00 2001 From: Alexy Legrand Date: Fri, 17 Oct 2025 09:42:39 +0200 Subject: [PATCH 10/15] use pathlib instead of os.path and add comments --- .../pinocchio/visualize/viser_visualizer.py | 70 ++++++++++--------- 1 file changed, 37 insertions(+), 33 deletions(-) diff --git a/bindings/python/pinocchio/visualize/viser_visualizer.py b/bindings/python/pinocchio/visualize/viser_visualizer.py index a60b2b662a..5d56020035 100644 --- a/bindings/python/pinocchio/visualize/viser_visualizer.py +++ b/bindings/python/pinocchio/visualize/viser_visualizer.py @@ -8,6 +8,7 @@ import numpy as np +from pathlib import Path from trimesh.exchange import dae from .. import pinocchio_pywrap_default as pin from . import BaseVisualizer @@ -220,14 +221,17 @@ def loadViewerGeometryObject(self, geometry_object, prefix="", color=None): def _add_mesh_from_path(self, name, mesh_path, color=None): """ - Load a mesh from a file. + Load a mesh from a file """ - ext = os.path.splitext(mesh_path)[1].lower() - + print("on passe la") + mesh_path = Path(mesh_path) + ext = mesh_path.suffix.lower() + if ext == ".dae": try: mesh_collada = collada.Collada(mesh_path) + # If there are more geometries than effects, fall back to trimesh if len(mesh_collada.effects) < len(mesh_collada.geometries): mesh = trimesh.load_mesh(mesh_path) if color is None: @@ -237,71 +241,69 @@ def _add_mesh_from_path(self, name, mesh_path, color=None): name, mesh.vertices, mesh.faces, color=color[:3], opacity=color[3] ) - + frames = [] - for i, (geometry, effect) in enumerate( - zip(mesh_collada.geometries, mesh_collada.effects) - ): + for i, (geometry, effect) in enumerate(zip(mesh_collada.geometries, mesh_collada.effects)): try: + # Extract vertices and indices from Collada geometry vertices = geometry.primitives[0].sources["VERTEX"][0][4].data indices = geometry.primitives[0].indices - + + # Handle different shapes of indices array if indices.ndim == 3: faces = indices[:, :, 0] - elif indices.ndim == 2: - faces = indices - elif indices.ndim == 1: + elif indices.ndim in (1, 2): faces = indices.reshape(-1, 3) else: faces = indices.reshape(-1, 3) - - if faces.shape[-1] != 3: - if faces.size % 3 == 0: - faces = faces.reshape(-1, 3) - + + # If after reshaping the last dimension isn't 3, try reshaping again + if faces.shape[-1] != 3 and faces.size % 3 == 0: + faces = faces.reshape(-1, 3) + mesh_color = getattr(effect, "diffuse", None) - + + # Add mesh with appropriate color if mesh_color is not None: frame = self.viewer.scene.add_mesh_simple( - f"{name}_{i}", - vertices, - faces, - color=mesh_color[:3], - opacity=mesh_color[3], + f"{name}_{i}", vertices, faces, + color=mesh_color[:3], opacity=mesh_color[3] ) elif color is not None: frame = self.viewer.scene.add_mesh_simple( - f"{name}_{i}", - vertices, - faces, - color=color[:3], - opacity=color[3], + f"{name}_{i}", vertices, faces, + color=color[:3], opacity=color[3] ) else: + # No color available, fallback to trimesh loader mesh = trimesh.load_mesh(mesh_path) frame = self.viewer.scene.add_mesh_trimesh(f"{name}_{i}", mesh) - + + # If for some reason the mesh wasn't added, use a tiny placeholder box if frame is None: frame = self.viewer.scene.add_box( f"{name}_{i}_placeholder", (0.001, 0.001, 0.001), color=(200, 200, 200) ) - + frames.append(frame) - + except Exception: + # Failed to process this geometry manually, fallback to trimesh try: mesh = trimesh.load_mesh(mesh_path) frame = self.viewer.scene.add_mesh_trimesh(f"{name}_{i}", mesh) if frame is not None: frames.append(frame) except Exception: + # Totally failed, skip this geometry pass - + return frames[0] if frames else None - + except Exception: + # Failed to load as Collada, fallback to trimesh for other formats or malformed .dae mesh = trimesh.load_mesh(mesh_path) if color is None: return self.viewer.scene.add_mesh_trimesh(name, mesh) @@ -310,7 +312,8 @@ def _add_mesh_from_path(self, name, mesh_path, color=None): name, mesh.vertices, mesh.faces, color=color[:3], opacity=color[3] ) - + + # For non-Collada files mesh = trimesh.load_mesh(mesh_path) if color is None: return self.viewer.scene.add_mesh_trimesh(name, mesh) @@ -320,6 +323,7 @@ def _add_mesh_from_path(self, name, mesh_path, color=None): color=color[:3], opacity=color[3] ) + def _add_mesh_from_convex(self, name, geom, color): """ Load a mesh from triangles stored inside a hppfcl.Convex. From bcf16810763111a9b4dfe6e9e9f835bb1e1ef224 Mon Sep 17 00:00:00 2001 From: Alexy Legrand Date: Fri, 17 Oct 2025 09:45:15 +0200 Subject: [PATCH 11/15] restore default robot after testing --- examples/gepetto-viewer.py | 4 ++-- examples/viser-viewer.py | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/examples/gepetto-viewer.py b/examples/gepetto-viewer.py index b996b9b946..95faf6ed3b 100644 --- a/examples/gepetto-viewer.py +++ b/examples/gepetto-viewer.py @@ -11,8 +11,8 @@ # Load the URDF model. model_path = Path(os.environ.get("EXAMPLE_ROBOT_DATA_MODEL_DIR")) mesh_dir = model_path.parent.parent -urdf_filename = "panda.urdf" -urdf_model_path = model_path / "panda_description/urdf" / urdf_filename +urdf_filename = "talos_reduced.urdf" +urdf_model_path = model_path / "talos_data/robots" / urdf_filename model, collision_model, visual_model = pin.buildModelsFromUrdf( urdf_model_path, mesh_dir, pin.JointModelFreeFlyer() diff --git a/examples/viser-viewer.py b/examples/viser-viewer.py index 33971c6470..b5500ddc19 100644 --- a/examples/viser-viewer.py +++ b/examples/viser-viewer.py @@ -17,8 +17,9 @@ mesh_dir = pinocchio_model_dir # urdf_filename = "talos_reduced.urdf" # urdf_model_path = join(join(model_path,"talos_data/robots"),urdf_filename) -urdf_filename = "tiago.urdf" -urdf_model_path = model_path / "tiago_description/robots" / urdf_filename +urdf_filename = "solo.urdf" +urdf_model_path = model_path / "solo_description/robots" / urdf_filename + model, collision_model, visual_model = pin.buildModelsFromUrdf( urdf_model_path, mesh_dir, pin.JointModelFreeFlyer() From e98d3282b44554e5217dfe21494173d234cc8b59 Mon Sep 17 00:00:00 2001 From: Alexy Legrand Date: Mon, 20 Oct 2025 08:40:06 +0200 Subject: [PATCH 12/15] remove useless print --- bindings/python/pinocchio/visualize/viser_visualizer.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bindings/python/pinocchio/visualize/viser_visualizer.py b/bindings/python/pinocchio/visualize/viser_visualizer.py index 5d56020035..8bf2004fe5 100644 --- a/bindings/python/pinocchio/visualize/viser_visualizer.py +++ b/bindings/python/pinocchio/visualize/viser_visualizer.py @@ -223,7 +223,6 @@ def _add_mesh_from_path(self, name, mesh_path, color=None): """ Load a mesh from a file """ - print("on passe la") mesh_path = Path(mesh_path) ext = mesh_path.suffix.lower() From 446c65892f7a480f20f8ead2a32af9867c71c3f8 Mon Sep 17 00:00:00 2001 From: Alexy Legrand Date: Mon, 20 Oct 2025 08:44:05 +0200 Subject: [PATCH 13/15] remove extra spaces --- examples/viser-viewer.py | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/viser-viewer.py b/examples/viser-viewer.py index b5500ddc19..dd4fbe20e1 100644 --- a/examples/viser-viewer.py +++ b/examples/viser-viewer.py @@ -20,7 +20,6 @@ urdf_filename = "solo.urdf" urdf_model_path = model_path / "solo_description/robots" / urdf_filename - model, collision_model, visual_model = pin.buildModelsFromUrdf( urdf_model_path, mesh_dir, pin.JointModelFreeFlyer() ) From b8e52741a2fc61e9e47b13ffe5bfcd41f95f0707 Mon Sep 17 00:00:00 2001 From: Alexy Legrand Date: Mon, 20 Oct 2025 09:40:06 +0200 Subject: [PATCH 14/15] simplify face indices normalization logic --- bindings/python/pinocchio/visualize/viser_visualizer.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/bindings/python/pinocchio/visualize/viser_visualizer.py b/bindings/python/pinocchio/visualize/viser_visualizer.py index 8bf2004fe5..943d812954 100644 --- a/bindings/python/pinocchio/visualize/viser_visualizer.py +++ b/bindings/python/pinocchio/visualize/viser_visualizer.py @@ -251,15 +251,10 @@ def _add_mesh_from_path(self, name, mesh_path, color=None): # Handle different shapes of indices array if indices.ndim == 3: faces = indices[:, :, 0] - elif indices.ndim in (1, 2): - faces = indices.reshape(-1, 3) else: faces = indices.reshape(-1, 3) - # If after reshaping the last dimension isn't 3, try reshaping again - if faces.shape[-1] != 3 and faces.size % 3 == 0: - faces = faces.reshape(-1, 3) - + # Extract color from effect or use provided color mesh_color = getattr(effect, "diffuse", None) # Add mesh with appropriate color From ea7a8810db5170f70cd7446136696f54e6553ed8 Mon Sep 17 00:00:00 2001 From: Alexy Legrand Date: Tue, 21 Oct 2025 09:29:54 +0200 Subject: [PATCH 15/15] add helper function to make the code more understandable --- .../pinocchio/visualize/viser_visualizer.py | 155 ++++++++---------- 1 file changed, 71 insertions(+), 84 deletions(-) diff --git a/bindings/python/pinocchio/visualize/viser_visualizer.py b/bindings/python/pinocchio/visualize/viser_visualizer.py index 943d812954..50d0543599 100644 --- a/bindings/python/pinocchio/visualize/viser_visualizer.py +++ b/bindings/python/pinocchio/visualize/viser_visualizer.py @@ -221,93 +221,81 @@ def loadViewerGeometryObject(self, geometry_object, prefix="", color=None): def _add_mesh_from_path(self, name, mesh_path, color=None): """ - Load a mesh from a file + Load a mesh from a file. """ - mesh_path = Path(mesh_path) - ext = mesh_path.suffix.lower() - + ext = os.path.splitext(mesh_path)[1].lower() + if ext == ".dae": - try: - mesh_collada = collada.Collada(mesh_path) - - # If there are more geometries than effects, fall back to trimesh - if len(mesh_collada.effects) < len(mesh_collada.geometries): - mesh = trimesh.load_mesh(mesh_path) - if color is None: - return self.viewer.scene.add_mesh_trimesh(name, mesh) - else: - return self.viewer.scene.add_mesh_simple( - name, mesh.vertices, mesh.faces, - color=color[:3], opacity=color[3] - ) - - frames = [] - for i, (geometry, effect) in enumerate(zip(mesh_collada.geometries, mesh_collada.effects)): - try: - # Extract vertices and indices from Collada geometry - vertices = geometry.primitives[0].sources["VERTEX"][0][4].data - indices = geometry.primitives[0].indices - - # Handle different shapes of indices array - if indices.ndim == 3: - faces = indices[:, :, 0] - else: - faces = indices.reshape(-1, 3) - - # Extract color from effect or use provided color - mesh_color = getattr(effect, "diffuse", None) - - # Add mesh with appropriate color - if mesh_color is not None: - frame = self.viewer.scene.add_mesh_simple( - f"{name}_{i}", vertices, faces, - color=mesh_color[:3], opacity=mesh_color[3] - ) - elif color is not None: - frame = self.viewer.scene.add_mesh_simple( - f"{name}_{i}", vertices, faces, - color=color[:3], opacity=color[3] - ) - else: - # No color available, fallback to trimesh loader - mesh = trimesh.load_mesh(mesh_path) - frame = self.viewer.scene.add_mesh_trimesh(f"{name}_{i}", mesh) - - # If for some reason the mesh wasn't added, use a tiny placeholder box - if frame is None: - frame = self.viewer.scene.add_box( - f"{name}_{i}_placeholder", - (0.001, 0.001, 0.001), - color=(200, 200, 200) - ) - - frames.append(frame) - - except Exception: - # Failed to process this geometry manually, fallback to trimesh - try: - mesh = trimesh.load_mesh(mesh_path) - frame = self.viewer.scene.add_mesh_trimesh(f"{name}_{i}", mesh) - if frame is not None: - frames.append(frame) - except Exception: - # Totally failed, skip this geometry - pass - - return frames[0] if frames else None + return self._load_collada_mesh(name, mesh_path, color) + else: + return self._load_standard_mesh(name, mesh_path, color) - except Exception: - # Failed to load as Collada, fallback to trimesh for other formats or malformed .dae + def _load_collada_mesh(self, name, mesh_path, color): + """ + Load a COLLADA mesh with color support. + """ + try: + mesh_collada = collada.Collada(mesh_path) + + if len(mesh_collada.effects) < len(mesh_collada.geometries): + return self._load_standard_mesh(name, mesh_path, color) + + frames = [] + for i, (geometry, effect) in enumerate(zip(mesh_collada.geometries, mesh_collada.effects)): + frame = self._process_collada_geometry(name, i, geometry, effect, color, mesh_path) + if frame: + frames.append(frame) + + return frames[0] if frames else None + except Exception: + return self._load_standard_mesh(name, mesh_path, color) + + def _process_collada_geometry(self, name, index, geometry, effect, fallback_color, mesh_path): + """ + Process a single COLLADA geometry with its material. + """ + try: + vertices, faces = self._extract_geometry_data(geometry) + mesh_color = getattr(effect, "diffuse", None) + + if mesh_color is not None: + return self.viewer.scene.add_mesh_simple( + f"{name}_{index}", vertices, faces, + color=mesh_color[:3], opacity=mesh_color[3] + ) + elif fallback_color is not None: + return self.viewer.scene.add_mesh_simple( + f"{name}_{index}", vertices, faces, + color=fallback_color[:3], opacity=fallback_color[3] + ) + else: mesh = trimesh.load_mesh(mesh_path) - if color is None: - return self.viewer.scene.add_mesh_trimesh(name, mesh) - else: - return self.viewer.scene.add_mesh_simple( - name, mesh.vertices, mesh.faces, - color=color[:3], opacity=color[3] - ) - - # For non-Collada files + return self.viewer.scene.add_mesh_trimesh(f"{name}_{index}", mesh) + except Exception: + try: + mesh = trimesh.load_mesh(mesh_path) + return self.viewer.scene.add_mesh_trimesh(f"{name}_{index}", mesh) + except Exception: + return None + + def _extract_geometry_data(self, geometry): + """ + Extract vertices and faces from a COLLADA geometry. + """ + vertices = geometry.primitives[0].sources["VERTEX"][0][4].data + indices = geometry.primitives[0].indices + + if indices.ndim == 3: + faces = indices[:, :, 0] + else: + faces = indices.reshape(-1, 3) + + return vertices, faces + + def _load_standard_mesh(self, name, mesh_path, color): + """ + Load a mesh using trimesh (STL and other formats). + """ mesh = trimesh.load_mesh(mesh_path) if color is None: return self.viewer.scene.add_mesh_trimesh(name, mesh) @@ -317,7 +305,6 @@ def _add_mesh_from_path(self, name, mesh_path, color=None): color=color[:3], opacity=color[3] ) - def _add_mesh_from_convex(self, name, geom, color): """ Load a mesh from triangles stored inside a hppfcl.Convex.