Source code for embodichain.lab.sim.utility.mesh_utils

# ----------------------------------------------------------------------------
# Copyright (c) 2021-2025 DexForce Technology Co., Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ----------------------------------------------------------------------------

import os
import dexsim.engine
import numpy as np
import open3d as o3d
import trimesh
import dexsim

from typing import Tuple, List, Dict, Any, Optional, Union

from embodichain.utils import logger


[docs] def export_articulation_mesh( articulation: Union[dexsim.engine.Articulation, list], output_path: str = "./articulation.obj", link_names: Optional[Union[List[str], Dict[Any, List[str]]]] = None, base_xpos: Optional[np.ndarray] = None, base_link_name: Optional[str] = None, **kwargs: Any, ) -> o3d.geometry.TriangleMesh: r"""Export a combined mesh from all links of one or more articulations to a mesh file format. This function retrieves the link geometries and poses from the given articulation(s), transforms each link mesh to its world pose, merges them into a single mesh, and exports the result to the specified file path. The export format is inferred from the file extension (e.g., .obj, .ply, .stl, .glb, .gltf). Args: articulation (dexsim.engine.Articulation or list): The articulation object or list of articulations. output_path (str): The output file path including the file name and extension. Supported extensions: .obj, .ply, .stl, .glb, .gltf. link_names (list[str] or dict[Any, list[str]], optional): Specify which links to export. If None, export all links. base_xpos (np.ndarray, optional): 4x4 homogeneous transformation matrix. All meshes will be transformed into this base pose coordinate system. base_link_name (str, optional): If specified, use the pose of this link as the base pose. The link will be searched from all link_names of all articulations. Returns: o3d.geometry.TriangleMesh: The combined Open3D mesh object of all articulations. """ output_path = os.path.abspath(output_path) os.makedirs(os.path.dirname(output_path), exist_ok=True) combined_mesh = o3d.geometry.TriangleMesh() articulations = ( articulation if isinstance(articulation, (list, tuple)) else [articulation] ) # Determine base transform: priority base_xpos > base_link_name > identity base_inv = None if base_xpos is not None: base_inv = np.linalg.inv(base_xpos) elif base_link_name is not None: # Search base_link_name from all link_names of all articulations found = False for art in articulations: # Get all possible link names for this articulation if link_names is None: cur_link_names = art.get_link_names() elif isinstance(link_names, dict): cur_link_names = link_names.get(art, art.get_link_names()) else: cur_link_names = link_names if base_link_name in cur_link_names: base_pose = art.get_link_pose(base_link_name) base_inv = np.linalg.inv(base_pose) found = True break if not found: logger.log_warning( f"base_link_name '{base_link_name}' not found in any articulation, using identity." ) base_inv = np.eye(4) else: base_inv = np.eye(4) for art in articulations: if link_names is None: cur_link_names = art.get_link_names() elif isinstance(link_names, dict): cur_link_names = link_names.get(art, art.get_link_names()) else: cur_link_names = link_names link_poses = [art.get_link_pose(name) for name in cur_link_names] for i, link_name in enumerate(cur_link_names): verts, faces = art.get_link_vert_face(link_name) logger.log_debug( f"Link '{link_name}' has {verts.shape[0]} vertices, {verts.shape[1]} faces." ) if verts.shape[0] == 0: continue mesh = o3d.geometry.TriangleMesh( o3d.utility.Vector3dVector(verts), o3d.utility.Vector3iVector(faces) ) mesh.compute_vertex_normals() mesh.transform(link_poses[i]) mesh.transform(base_inv) combined_mesh += mesh combined_mesh.compute_vertex_normals() ext = os.path.splitext(output_path)[1].lower() if ext in [".obj", ".ply", ".stl"]: o3d.io.write_triangle_mesh(output_path, combined_mesh) logger.log_info(f"Mesh exported using Open3D to: {output_path}") elif ext in [".glb", ".gltf"]: mesh_trimesh = trimesh.Trimesh( vertices=np.asarray(combined_mesh.vertices), faces=np.asarray(combined_mesh.triangles), vertex_normals=np.asarray(combined_mesh.vertex_normals), ) mesh_trimesh.export(output_path) logger.log_info(f"Mesh exported using trimesh to: {output_path}") else: raise ValueError( f"Unsupported file format: '{ext}'. Supported: obj, ply, stl, glb, gltf" ) return combined_mesh