-
Notifications
You must be signed in to change notification settings - Fork 2.6k
[Feature] Usd Stage Import Support: Articulation & RigidBody #2067
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
|
can you add a unit test? The assets can be put into the huggingface repo. An example is like this Genesis/tests/test_rigid_physics.py Line 581 in 4922239
|
| from pxr import Usd, UsdGeom, Gf | ||
| from typing import List, Tuple | ||
| import genesis as gs | ||
| import numpy as np | ||
| import trimesh | ||
| from collections import deque | ||
| from .. import geom as gu | ||
| import scipy.linalg |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should first import standards libraries, skip line, then ubiquitous third-party libraries (torch, numpy, tqdm...etc), skip line, then random third-party libraries, skip line, then our own custom libraries, skip line, then relative genesis imports. For each group, make sure it is sorted alphabetically.
|
|
||
| def usd_quat_to_numpy(usd_quat: Gf.Quatf) -> np.ndarray: | ||
| """ | ||
| Convert a USD Gf.Quatf to a numpy array (w, x, y, z format). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(w, x, y, z) format
| R, S = scipy.linalg.polar(trans_matrix[:3, :3], side="right") | ||
| return R, S |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoid temporary if not necessary.
Moreover, scipy should NOT be considered part of our dependencies. Avoid relying on it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you need more helpers, add them to gs.utils.geom.
| """ | ||
|
|
||
| # Compute Genesis transform relative to ref_prim (Q^i_j) | ||
| Q_rel, S = compute_gs_related_transform(usd_mesh.GetPrim(), ref_prim) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"Genesis transform" is weird naming. Could you stick on mathematically accurate description? Same for compute_gs_related_transform: gs_related is not a good name.
| points = np.array(points_attr.Get()) | ||
| # Apply only scaling to every point | ||
| points = points @ S | ||
| face_vertex_counts = np.array(face_vertex_counts_attr.Get()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Prefer using np.asarray / np.asanyarray when passing generic objects.
| has_non_tri_quads = False | ||
| for i, count in enumerate(face_vertex_counts): | ||
| face_vertex_counts[i] = count | ||
| if count == 3: | ||
| # Triangle - use directly | ||
| faces.append(face_vertex_indices[offset : offset + count]) | ||
| elif count == 4: | ||
| # Quad - split into two triangles | ||
| quad = face_vertex_indices[offset : offset + count] | ||
| faces.append([quad[0], quad[1], quad[2]]) | ||
| faces.append([quad[0], quad[2], quad[3]]) | ||
| elif count > 4: | ||
| # Polygon with more than 4 vertices - triangulate using triangle fan | ||
| # Use the first vertex as the fan center and connect to each pair of consecutive vertices | ||
| polygon = face_vertex_indices[offset : offset + count] | ||
| for j in range(1, count - 1): | ||
| faces.append([polygon[0], polygon[j], polygon[j + 1]]) | ||
| has_non_tri_quads = True | ||
| else: | ||
| # Invalid face (count < 3) | ||
| gs.logger.warning(f"Invalid face vertex count {count} in USD mesh {usd_mesh.GetPath()}. Skipping face.") | ||
| offset += count |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This for-loop does not look efficient at all. It is fast enough even for complex mesh?
| def apply_transform_to_pos(trans_matrix: np.ndarray, pos: np.ndarray) -> np.ndarray: | ||
| """ | ||
| Apply a transformation matrix to a position. | ||
| Parameters | ||
| ---------- | ||
| trans_matrix : np.ndarray, shape (4, 4) or (3, 3) | ||
| pos : np.ndarray, shape (3,) | ||
| The position to apply the transformation to. | ||
| Returns | ||
| ------- | ||
| np.ndarray, shape (3,) | ||
| The transformed position. | ||
| """ | ||
| return trans_matrix[:3, :3] @ pos + trans_matrix[:3, 3] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we already have a helper function for this in gs.utils.geom. Why reimplement this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
transform_by_T
| def extract_quat_from_transform(trans_matrix: np.ndarray) -> np.ndarray: | ||
| """ | ||
| Extract quaternion from a 4x4 transformation matrix. | ||
| Parameters | ||
| ---------- | ||
| trans_matrix : np.ndarray, shape (4, 4) or (3, 3) | ||
| The transformation matrix. | ||
| Returns | ||
| ------- | ||
| np.ndarray, shape (4,) | ||
| Quaternion as numpy array [w, x, y, z]. | ||
| """ | ||
| R, _ = extract_rotation_and_scale(trans_matrix) | ||
| quat = gu.R_to_quat(R) | ||
| return quat |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same. Just call gu.R_to_quat. It is already scale-insensitive.
| t = imageable.ComputeLocalToWorldTransform(Usd.TimeCode.Default()).GetTranspose() | ||
| return np.array(t) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return np.asarray(imageable.ComputeLocalToWorldTransform(Usd.TimeCode.Default()).GetTranspose())| prim_to_ref_prim_transform = world_to_ref_prim @ prim_world_transform | ||
| return prim_to_ref_prim_transform |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove temporary.
| return np.array(t) | ||
|
|
||
|
|
||
| def compute_usd_related_transform(prim: Usd.Prim, ref_prim: Usd.Prim | None) -> np.ndarray: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
usd_related is not a good function name. Use a mathematical description.
| return Q_i_j, S_prim | ||
|
|
||
|
|
||
| def convert_usd_joint_axis_to_gs(usd_local_joint_axis: np.ndarray, usd_link_prim: Usd.Prim | None) -> np.ndarray: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same. This function names are confusing. We cannot tell straight away what they are computing, which is problematic. Even the docstring does not explain what is the difference between "USD link local space" and "Genesis link local space".
| def compute_joint_axis_scaling_factor(gs_local_joint_axis: np.ndarray) -> float: | ||
| """ | ||
| Compute the scaling factor for a joint axis. | ||
| Parameters | ||
| ---------- | ||
| gs_local_joint_axis : np.ndarray, shape (3,) | ||
| The joint axis in Genesis link local space. | ||
| Returns | ||
| ------- | ||
| float | ||
| The scaling factor. | ||
| """ | ||
| return np.linalg.norm(gs_local_joint_axis) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not convinced adding a function abstraction of top of a simple norm is helpful but ok.
| class UsdGeometryAdapter: | ||
| """ | ||
| A adapter to convert USD geometry to Genesis geometry info. | ||
| Receive: UsdGeom.Mesh, UsdGeom.Plane, UsdGeom.Sphere, UsdGeom.Capsule, UsdGeom.Cube | ||
| Return: Genesis geometry info | ||
| """ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need this? It is confusing.
| elif self._prim.IsA(UsdGeom.Plane): | ||
| r = self._create_gs_plane_geo_info() | ||
| g_info.update(r) | ||
| elif self._prim.IsA(UsdGeom.Sphere): | ||
| r = self._create_gs_sphere_geo_info() | ||
| g_info.update(r) | ||
| elif self._prim.IsA(UsdGeom.Capsule): | ||
| r = self._create_gs_capsule_geo_info() | ||
| g_info.update(r) | ||
| elif self._prim.IsA(UsdGeom.Cube): | ||
| r = self._create_gs_cube_geo_info() | ||
| g_info.update(r) | ||
| elif self._prim.IsA(UsdGeom.Cylinder): | ||
| r = self._create_gs_cylinder_geo_info() | ||
| g_info.update(r) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are you prefixing all your function with _gs_? Of course it is creating Genesis objects, what else it could be? We only have a single simulator.
| gs.logger.warning( | ||
| f"Size of uvs mismatch for mesh {mesh_prim.GetPath()} in usd file {self._get_usd_file_path()}." | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not an exception? Is this kind of issue very common? Because we need to have a good motivation to be fault tolerant.
|
|
||
| # Triangulate faces | ||
| if len(face_vertex_counts) == 0: | ||
| triangles = np.array([], dtype=np.int32).reshape(0, 3) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
triangles = np.zeros((0, 3), dtype=np.int32)
| triangles = np.array(triangles, dtype=np.int32) | ||
| else: | ||
| triangles = face_vertex_indices.reshape(-1, 3) | ||
| # Get UV name from material (needed for UV extraction) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is this comment?
| gs.logger.warning( | ||
| f"Sphere: {self._prim.GetPath()} scale is not uniform: {S}, take the mean of the three components" | ||
| ) | ||
| radius *= np.mean(S_diag) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't like this kind of fallback. If you cannot parse a file properly, just raise an exception. Hiding failure is rarely desirable.
| # Get capsule dimensions (defaults) | ||
| radius = radius_attr.Get() if radius_attr and radius_attr.HasValue() else 0.5 | ||
| height = height_attr.Get() if height_attr and height_attr.HasValue() else 1.0 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From where are these default coming from? Could you are a URL for reference?
| # Get cylinder dimensions (defaults) | ||
| radius = radius_attr.Get() if radius_attr and radius_attr.HasValue() else 0.5 | ||
| height = height_attr.Get() if height_attr and height_attr.HasValue() else 1.0 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same. Add reference for default values.
| class BatchedUsdGeometryAdapater: | ||
| """ | ||
| A adapter to convert USD geometry to Genesis geometry info. | ||
| Receive: List[UsdGeom.Mesh], List[UsdGeom.Plane], List[UsdGeom.Sphere], List[UsdGeom.Capsule], List[UsdGeom.Cube] | ||
| Return: List[Dict] | ||
| """ | ||
|
|
||
| def __init__( | ||
| self, ctx: UsdParserContext, start_prim: Usd.Prim, ref_prim: Usd.Prim, mesh_type: Literal["mesh", "vmesh"] | ||
| ): | ||
| self._ctx: UsdParserContext = ctx | ||
| self._start_prim: Usd.Prim = start_prim | ||
| self._ref_prim: Usd.Prim = ref_prim | ||
| self._mesh_type: Literal["mesh", "vmesh"] = mesh_type | ||
| self._geometries: List[Usd.Prim] = self._find_all_geometries() | ||
|
|
||
| def _find_all_geometries(self) -> List[Usd.Prim]: | ||
| """Find all geometries under the start prim.""" | ||
| geometries: List[Usd.Prim] = [] | ||
|
|
||
| # consider the start prim itself | ||
| for geom_type in UsdGeometryAdapter.SupportUsdGeoms: | ||
| if self._start_prim.IsA(geom_type): | ||
| geometries.append(self._start_prim) | ||
| break | ||
|
|
||
| # consider the children of the start prim | ||
| for prim in bfs_iterator(self._start_prim): | ||
| for geom_type in UsdGeometryAdapter.SupportUsdGeoms: | ||
| if prim.IsA(geom_type): | ||
| geometries.append(prim) | ||
| break | ||
| return geometries | ||
|
|
||
| def create_gs_geo_infos(self) -> List[Dict]: | ||
| """Create geometry info for all geometries.""" | ||
| g_infos: List[Dict] = [] | ||
| for geometry in self._geometries: | ||
| g_info = UsdGeometryAdapter(self._ctx, geometry, self._ref_prim, self._mesh_type).create_gs_geo_info() | ||
| assert g_info is not None, f"Geometry: {geometry.GetPath()} create gs geo info failed" | ||
| g_infos.append(g_info) | ||
| return g_infos |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this a class instead of a function?
| g_infos: List[Dict] = [] | ||
| for geometry in self._geometries: | ||
| g_info = UsdGeometryAdapter(self._ctx, geometry, self._ref_prim, self._mesh_type).create_gs_geo_info() | ||
| assert g_info is not None, f"Geometry: {geometry.GetPath()} create gs geo info failed" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Prefer raising exception, unless it mainly serves as documentation and should never happen in practice.
| import genesis as gs | ||
| import numpy as np | ||
| import re | ||
| from scipy.spatial.transform import Rotation as R |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do not use scipy.
| from pxr import Usd, UsdGeom, UsdPhysics, UsdShade, Sdf | ||
| from typing import List, Dict, Tuple, Literal | ||
| import genesis as gs | ||
| import numpy as np | ||
| import re | ||
| from scipy.spatial.transform import Rotation as R | ||
| from .usd_parser_context import UsdParserContext | ||
| from .usd_parser_utils import ( | ||
| bfs_iterator, | ||
| compute_gs_related_transform, | ||
| extract_quat_from_transform, | ||
| compute_gs_global_transform, | ||
| convert_usd_joint_axis_to_gs, | ||
| usd_quat_to_numpy, | ||
| convert_usd_joint_pos_to_gs, | ||
| ) | ||
| from .usd_geo_adapter import BatchedUsdGeometryAdapater, UsdGeometryAdapter | ||
| from .. import geom as gu | ||
| from .. import urdf as urdf_utils |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix order of imports.
|
|
||
| class UsdArticulationParser: | ||
| """ | ||
| A parser to extract articulation information from a USD stage. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"articulation information" is not clear. Be more explicit.
| f"Provided prim {articulation_root_prim.GetPath()} is not an Articulation Root. Now we only support articulation parsing from ArticulationRootAPI." | ||
| ) | ||
|
|
||
| gs.logger.info(f"Parsing USD articulation from {articulation_root_prim.GetPath()}.") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.debug
| self._root: Usd.Prim = articulation_root_prim | ||
| if not articulation_root_prim.HasAPI(UsdPhysics.ArticulationRootAPI): | ||
| gs.raise_exception( | ||
| f"Provided prim {articulation_root_prim.GetPath()} is not an Articulation Root. Now we only support articulation parsing from ArticulationRootAPI." |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Line is too long.
| class UsdArticulationParser: | ||
| """ | ||
| A parser to extract articulation information from a USD stage. | ||
| The Parser is agnostic to genesis structures, it only focuses on USD articulation structure. | ||
| """ | ||
|
|
||
| def __init__(self, stage: Usd.Stage, articulation_root_prim: Usd.Prim): | ||
| """ | ||
| Initialize the articulation parser. | ||
| Parameters | ||
| ---------- | ||
| stage : Usd.Stage | ||
| The USD stage. | ||
| articulation_root_prim : Usd.Prim | ||
| The root prim of the articulation (must have ArticulationRootAPI). | ||
| """ | ||
| self._stage: Usd.Stage = stage | ||
| self._root: Usd.Prim = articulation_root_prim | ||
| if not articulation_root_prim.HasAPI(UsdPhysics.ArticulationRootAPI): | ||
| gs.raise_exception( | ||
| f"Provided prim {articulation_root_prim.GetPath()} is not an Articulation Root. Now we only support articulation parsing from ArticulationRootAPI." | ||
| ) | ||
|
|
||
| gs.logger.info(f"Parsing USD articulation from {articulation_root_prim.GetPath()}.") | ||
|
|
||
| self.joints: List[UsdPhysics.Joint] = [] | ||
| self.fixed_joints: List[UsdPhysics.FixedJoint] = [] | ||
| self.revolute_joints: List[UsdPhysics.RevoluteJoint] = [] | ||
| self.prismatic_joints: List[UsdPhysics.PrismaticJoint] = [] | ||
| self.spherical_joints: List[UsdPhysics.SphericalJoint] = [] | ||
| self._collect_joints() | ||
|
|
||
| self.links: List[Usd.Prim] = [] | ||
| self._collect_links() | ||
|
|
||
| # ==================== Static Methods: Finding Articulation Roots ==================== | ||
|
|
||
| @staticmethod | ||
| def find_all_articulation_roots(stage: Usd.Stage, context: UsdParserContext = None) -> List[Usd.Prim]: | ||
| """ | ||
| Find all prims with ArticulationRootAPI in the stage. | ||
| Parameters | ||
| ---------- | ||
| stage : Usd.Stage | ||
| The USD stage. | ||
| context : UsdParserContext, optional | ||
| If provided, articulation roots will be added to the context. | ||
| Returns | ||
| ------- | ||
| List[Usd.Prim] | ||
| List of articulation root prims. | ||
| """ | ||
| articulation_roots = [] | ||
| for prim in bfs_iterator(stage.GetPseudoRoot()): | ||
| if prim.HasAPI(UsdPhysics.ArticulationRootAPI): | ||
| articulation_roots.append(prim) | ||
| if context: | ||
| context.add_articulation_root(prim) | ||
| return articulation_roots | ||
|
|
||
| # ==================== Collection Methods: Joints and Links ==================== | ||
|
|
||
| def _collect_joints(self): | ||
| """Collect all joints in the articulation.""" | ||
| for child in bfs_iterator(self._root): | ||
| if child.IsA(UsdPhysics.Joint): | ||
| joint_api = UsdPhysics.Joint(child) | ||
| self.joints.append(joint_api) | ||
| if child.IsA(UsdPhysics.RevoluteJoint): | ||
| revolute_joint_api = UsdPhysics.RevoluteJoint(child) | ||
| self.revolute_joints.append(revolute_joint_api) | ||
| elif child.IsA(UsdPhysics.FixedJoint): | ||
| fixed_joint_api = UsdPhysics.FixedJoint(child) | ||
| self.fixed_joints.append(fixed_joint_api) | ||
| elif child.IsA(UsdPhysics.PrismaticJoint): | ||
| prismatic_joint_api = UsdPhysics.PrismaticJoint(child) | ||
| self.prismatic_joints.append(prismatic_joint_api) | ||
| elif child.IsA(UsdPhysics.SphericalJoint): | ||
| spherical_joint_api = UsdPhysics.SphericalJoint(child) | ||
| self.spherical_joints.append(spherical_joint_api) | ||
|
|
||
| def _collect_links(self): | ||
| """Collect all links connected by joints in the articulation.""" | ||
| # Now we have joints collected, we can find links connected by these joints | ||
| paths = set() | ||
| for joint in self.joints: | ||
| body0_targets = joint.GetBody0Rel().GetTargets() | ||
| body1_targets = joint.GetBody1Rel().GetTargets() | ||
| for target_path in body0_targets + body1_targets: | ||
| # Check target is valid | ||
| if self._stage.GetPrimAtPath(target_path): | ||
| paths.add(target_path) | ||
| else: | ||
| gs.raise_exception(f"Joint {joint.GetPath()} has invalid target body reference {target_path}.") | ||
| for path in paths: | ||
| prim = self._stage.GetPrimAtPath(path) | ||
| self.links.append(prim) | ||
|
|
||
| # ==================== Geometry Collection Methods ==================== | ||
|
|
||
| visual_pattern = re.compile(r"^(visual|Visual).*") | ||
| collision_pattern = re.compile(r"^(collision|Collision).*") | ||
| all_pattern = re.compile(r"^.*") | ||
|
|
||
| @staticmethod | ||
| def create_gs_geo_infos( | ||
| context: UsdParserContext, link: Usd.Prim, pattern, mesh_type: Literal["mesh", "vmesh"] | ||
| ) -> List[Dict]: | ||
| # if the link itself is a geometry | ||
| geo_infos: List[Dict] = [] | ||
| link_geo_adapter = UsdGeometryAdapter(context, link, link, mesh_type) | ||
| link_geo_info = link_geo_adapter.create_gs_geo_info() | ||
| if link_geo_info is not None: | ||
| geo_infos.append(link_geo_info) | ||
|
|
||
| # - Link | ||
| # - Visuals | ||
| # - Collisions | ||
| search_roots: list[Usd.Prim] = [] | ||
| for child in link.GetChildren(): | ||
| if pattern.match(child.GetName()): | ||
| search_roots.append(child) | ||
|
|
||
| for search_root in search_roots: | ||
| adapter = BatchedUsdGeometryAdapater(context, search_root, link, mesh_type) | ||
| geo_infos.extend(adapter.create_gs_geo_infos()) | ||
|
|
||
| return geo_infos | ||
|
|
||
| @staticmethod | ||
| def get_visual_geometries(link: Usd.Prim, context: UsdParserContext) -> List[Dict]: | ||
| if context.vis_mode == "visual": | ||
| vis_geo_infos = UsdArticulationParser.create_gs_geo_infos( | ||
| context, link, UsdArticulationParser.visual_pattern, "vmesh" | ||
| ) | ||
| if len(vis_geo_infos) == 0: | ||
| # if no visual geometries found, use any pattern to find visual geometries | ||
| gs.logger.info( | ||
| f"No visual geometries found, using any pattern to find visual geometries in {link.GetPath()}" | ||
| ) | ||
| vis_geo_infos = UsdArticulationParser.create_gs_geo_infos( | ||
| context, link, UsdArticulationParser.all_pattern, "vmesh" | ||
| ) | ||
| elif context.vis_mode == "collision": | ||
| vis_geo_infos = UsdArticulationParser.create_gs_geo_infos( | ||
| context, link, UsdArticulationParser.collision_pattern, "vmesh" | ||
| ) | ||
| else: | ||
| gs.raise_exception(f"Unsupported visualization mode {context.vis_mode}.") | ||
| return vis_geo_infos | ||
|
|
||
| @staticmethod | ||
| def get_collision_geometries(link: Usd.Prim, context: UsdParserContext) -> List[Dict]: | ||
| col_geo_infos = UsdArticulationParser.create_gs_geo_infos( | ||
| context, link, UsdArticulationParser.collision_pattern, "mesh" | ||
| ) | ||
| return col_geo_infos |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand why this has to be a class.
| if axis_str == "X": | ||
| return np.array([1.0, 0.0, 0.0]) | ||
| elif axis_str == "Y": | ||
| return np.array([0.0, 1.0, 0.0]) | ||
| elif axis_str == "Z": | ||
| return np.array([0.0, 0.0, 1.0]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should always specify dtype when allocating memory. In this case, either using gs.np_float or np.float32, np.float64 if it has to be more specific.
Description
This PR adds comprehensive USD (Universal Scene Description) import functionality to Genesis, enabling users to directly import USD stages containing articulated structures and rigid bodies into Genesis simulation scenes.
The implementation includes:
Scene.add_stage()method for easy USD file importUSDArticulationandUSDRigidBodymorph classes for USD-based entitiesKey components:
UsdParser: Main parser entrance withimport_from_stage()methodUsdArticulationParser: Extracts and parses articulation structures from USD stagesUsdRigidBodyParser: Extracts and parses rigid body prims from USD stagesUsdRenderingMaterialParser: Parses USD materials and shadersUsdParserContext: Context manager for tracking parsed entities and materialsUsdParserUtils: Utility functions for transform computation, mesh conversion, and coordinate system transformationsMotivation and Context
USD is a widely-used format in the robotics and simulation community, particularly in tools like NVIDIA Isaac Sim, Omniverse, and other industry-standard platforms. Adding USD import capability allows Genesis users to:
This feature significantly improves Genesis's interoperability with the broader robotics and simulation ecosystem.
How Has This Been / Can This Be Tested?
The feature can be tested using the provided example script:
Testing Checklist:
Test Files:
examples/usd/import_stage.pyScreenshots (if appropriate):
20251202-115133.mp4
Checklist:
I read the CONTRIBUTING document.
I followed the
Submitting Code Changessection of CONTRIBUTING document.I tagged the title correctly (including BUG FIX/FEATURE/MISC/BREAKING)
I updated the documentation accordingly or no change is needed.
I tested my changes and added instructions on how to test it for reviewers.
I have added tests to cover my changes.
All new and existing tests passed.