Source code for mani_skill.utils.building._mjcf_loader

"""
Loader code to import MJCF xml files into SAPIEN

Code partially adapted from https://github.com/NVIDIA/warp/blob/3ed2ceab824b65486c5204d2a7381d37b79fc314/warp/sim/import_mjcf.py

Articulations are known as kinematic trees (defined by <body> tags) in Mujoco. A single .xml file can have multiple articulations

Any <geom> tag in <worldbody> but not a <body> tag will be built as separate static actors if possible. Actors that are not static seem to be defined
with a free joint under a single body tag.

Warnings of unloadable tags/data can be printed if verbosity is turned on (by default it is off)

Notes:
    Joint properties relating to the solver, stiffness, actuator, are all not directly imported here
    and instead must be implemented via a controller like other robots in SAPIEN

    Contact tags are not supported

    Tendons/equality constraints are supported but may not work the same

    The default group of geoms is 0 in mujoco. From docs it appears only group 0 and 2 are rendered by default.
    This is also by default what the visualizer shows and presumably what image renders show.
    Any other group is treated as being invisible (e.g. in SAPIEN we do not add visual bodies). SAPIEN does not currently support
    toggling render groups like Mujoco. Sometimes a MJCF might not follow this and will try and render other groups. In that case the loader supports
    indicating which other groups to add visual bodies for.

    Ref: https://mujoco.readthedocs.io/en/stable/XMLreference.html#body-geom-group,
    https://mujoco.readthedocs.io/en/latest/modeling.html#composite-objects (says group 3 is turned off)

    If contype is 0, it means that geom can't collide with anything. We do this by not adding a collision shape at all.

    geoms under worldbody but not body tags are treated as static objects at the moment.

    Useful references:
    - Collision detection: https://mujoco.readthedocs.io/en/stable/computation/index.html#collision-detection


"""
import math
import os
import re
import xml.etree.ElementTree as ET
from collections import defaultdict
from copy import deepcopy
from dataclasses import dataclass
from functools import reduce
from typing import Any, Literal, Tuple, Union
from xml.etree.ElementTree import Element

import numpy as np
import sapien
from sapien import ActorBuilder, Pose
from sapien.physx import PhysxArticulation, PhysxMaterial
from sapien.render import RenderMaterial, RenderTexture2D
from sapien.wrapper.articulation_builder import (
    ArticulationBuilder,
    LinkBuilder,
    MimicJointRecord,
)
from transforms3d import euler, quaternions

from mani_skill import logger


@dataclass
[docs]class MJCFTexture:
[docs] name: str
[docs] type: Literal["skybox", "cube", "2d"]
[docs] rgb1: list
[docs] rgb2: list
[docs] file: str
[docs]DEFAULT_MJCF_OPTIONS = dict(contact=True)
[docs]WARNED_ONCE = defaultdict(lambda: False)
[docs]def _parse_int(attrib, key, default): if key in attrib: return int(attrib[key]) else: return default
[docs]def _parse_float(attrib, key, default): if key in attrib: return float(attrib[key]) else: return default
[docs]def _str_to_float(string: str, delimiter=" "): res = [float(x) for x in string.split(delimiter)] if len(res) == 1: return res[0] return res
[docs]def _merge_attrib(default_attrib: dict, incoming_attribs: Union[list[dict], dict]): def helper_merge(a: dict, b: dict, path=[]): for key in b: if key in a: if isinstance(a[key], dict) and isinstance(b[key], dict): helper_merge(a[key], b[key], path + [str(key)]) else: a[key] = b[key] else: a[key] = b[key] return a attrib = deepcopy(default_attrib) if isinstance(incoming_attribs, dict): incoming_attribs = [incoming_attribs] reduce(helper_merge, [attrib] + incoming_attribs) return attrib
[docs]def _parse_vec(attrib, key, default): if key in attrib: out = np.fromstring(attrib[key], sep=" ", dtype=np.float32) else: out = np.array(default, dtype=np.float32) return out
[docs]def _parse_orientation(attrib, use_degrees, euler_seq): if "quat" in attrib: wxyz = np.fromstring(attrib["quat"], sep=" ") return wxyz if "euler" in attrib: euler_angles = np.fromstring(attrib["euler"], sep=" ") if use_degrees: euler_angles *= np.pi / 180 # TODO (stao): support other axes? return np.array( euler.euler2quat(euler_angles[0], -euler_angles[1], euler_angles[2]) ) if "axisangle" in attrib: axisangle = np.fromstring(attrib["axisangle"], sep=" ") angle = axisangle[3] if use_degrees: angle *= np.pi / 180 axis = axisangle[:3] / np.linalg.norm(axisangle[:3]) return quaternions.axangle2quat(axis, angle) if "xyaxes" in attrib: xyaxes = np.fromstring(attrib["xyaxes"], sep=" ") xaxis = xyaxes[:3] / np.linalg.norm(xyaxes[:3]) zaxis = xyaxes[3:] / np.linalg.norm(xyaxes[:3]) yaxis = np.cross(zaxis, xaxis) yaxis = yaxis / np.linalg.norm(yaxis) rot_matrix = np.array([xaxis, yaxis, zaxis]).T return quaternions.mat2quat(rot_matrix) if "zaxis" in attrib: zaxis = np.fromstring(attrib["zaxis"], sep=" ") zaxis = zaxis / np.linalg.norm(zaxis) xaxis = np.cross(np.array([0, 0, 1]), zaxis) xaxis = xaxis / np.linalg.norm(xaxis) yaxis = np.cross(zaxis, xaxis) yaxis = yaxis / np.linalg.norm(yaxis) rot_matrix = np.array([xaxis, yaxis, zaxis]).T return quaternions.mat2quat(rot_matrix) return np.array([1, 0, 0, 0])
[docs]class MJCFLoader: """ Class to load MJCF into SAPIEN. """ def __init__(self, ignore_classes=["motor"], visual_groups=[0, 2]): """whether to fix the root link. Note regardless of given XML, the root link is a dummy link this loader creates which makes a number of operations down the line easier. In general this should be False if there is a freejoint for the root body of articulations in the XML and should be true if there are no free joints. At the moment when modelling a robot from Mujoco this must be handled on a case by case basis"""
[docs] self.load_multiple_collisions_from_file = False
[docs] self.load_nonconvex_collisions = False
[docs] self.multiple_collisions_decomposition = "none"
[docs] self.multiple_collisions_decomposition_params = dict()
[docs] self.revolute_unwrapped = False
[docs] self.scale = 1.0
[docs] self.visual_groups = visual_groups
[docs] self.scene: sapien.Scene = None
[docs] self.ignore_classes = ignore_classes
# self._material = None # self._patch_radius = 0 # self._min_patch_radius = 0
[docs] self.density = 1000
# self._link_material = dict() # self._link_patch_radius = dict() # self._link_min_patch_radius = dict() # self._link_density = dict()
[docs] self._defaults: dict[str, Element] = dict()
[docs] self._assets = dict()
[docs] self._materials = dict()
[docs] self._textures: dict[str, MJCFTexture] = dict()
[docs] self._meshes: dict[str, Element] = dict()
[docs] self._link2builder: dict[str, LinkBuilder] = dict()
[docs] self._link2parent_joint: dict[str, Any] = dict()
[docs] self._group_count = 0
[docs] def set_scene(self, scene): self.scene = scene return self
@staticmethod
[docs] def _pose_from_origin(origin, scale): origin[:3, 3] = origin[:3, 3] * scale return Pose(origin)
[docs] def _build_geom( self, geom: Element, builder: Union[LinkBuilder, ActorBuilder], defaults ): geom_defaults = defaults if "class" in geom.attrib: geom_class = geom.attrib["class"] ignore_geom = False for pattern in self.ignore_classes: if re.match(pattern, geom_class): ignore_geom = True break if ignore_geom: return if geom_class in self._defaults: geom_defaults = _merge_attrib(defaults, self._defaults[geom_class]) if "geom" in geom_defaults: geom_attrib = _merge_attrib(geom_defaults["geom"], geom.attrib) else: geom_attrib = geom.attrib geom_name = geom_attrib.get("name", "") geom_type = geom_attrib.get("type", "sphere") if "mesh" in geom_attrib: geom_type = "mesh" geom_size = ( _parse_vec(geom_attrib, "size", np.array([1.0, 1.0, 1.0])) * self.scale ) geom_pos = ( _parse_vec(geom_attrib, "pos", np.array([0.0, 0.0, 0.0])) * self.scale ) geom_rot = _parse_orientation(geom_attrib, self._use_degrees, self._euler_seq) _parse_float(geom_attrib, "density", self.density) if "material" in geom_attrib: render_material = self._materials[geom_attrib["material"]] else: # use RGBA render_material = RenderMaterial( base_color=_parse_vec(geom_attrib, "rgba", [0.5, 0.5, 0.5, 1]) ) geom_density = _parse_float(geom_attrib, "density", 1000.0) # if condim is 1, we can easily model the material's friction condim = _parse_int(geom_attrib, "condim", 3) if condim == 3: friction = _parse_vec( geom_attrib, "friction", np.array([0.3, 0.3, 0.3]) ) # maniskill default friction is 0.3 # NOTE (stao): we only support sliding friction at the moment. see # https://mujoco.readthedocs.io/en/stable/XMLreference.html#body-geom-friction # we might be able to imitate their torsional frictions via patch radius attributes: # https://nvidia-omniverse.github.io/PhysX/physx/5.4.0/_api_build/class_px_shape.html#_CPPv4N7PxShape23setTorsionalPatchRadiusE6PxReal friction = friction[0] physx_material = PhysxMaterial( static_friction=friction, dynamic_friction=friction, restitution=0 ) elif condim == 1: physx_material = PhysxMaterial( static_friction=0, dynamic_friction=0, restitution=0 ) else: physx_material = None geom_group = _parse_int(geom_attrib, "group", 0) # See note at top of file for how we handle geom groups has_visual_body = False if geom_group in self.visual_groups: has_visual_body = True geom_contype = _parse_int(geom_attrib, "contype", 1) # See note at top of file for how we handle contype / objects without collisions has_collisions = True if geom_contype == 0: has_collisions = False t_visual2link = Pose(geom_pos, geom_rot) if geom_type == "sphere": if has_visual_body: builder.add_sphere_visual( t_visual2link, radius=geom_size[0], material=render_material ) if has_collisions: builder.add_sphere_collision( t_visual2link, radius=geom_size[0], material=physx_material, density=geom_density, ) elif geom_type in ["capsule", "cylinder", "box"]: if "fromto" in geom_attrib: geom_fromto = _parse_vec( geom_attrib, "fromto", (0.0, 0.0, 0.0, 1.0, 0.0, 0.0) ) start = np.array(geom_fromto[0:3]) * self.scale end = np.array(geom_fromto[3:6]) * self.scale # objects follow a line from start to end. # objects are default along x-axis and we rotate accordingly via axis angle. axis = (end - start) / np.linalg.norm(end - start) # TODO this is bugged angle = math.acos(np.dot(axis, np.array([1.0, 0.0, 0.0]))) axis = np.cross(axis, np.array([1.0, 0.0, 0.0])) if np.linalg.norm(axis) < 1e-3: axis = np.array([1, 0, 0]) else: axis = axis / np.linalg.norm(axis) geom_pos = (start + end) * 0.5 geom_rot = quaternions.axangle2quat(axis, -angle) t_visual2link.set_p(geom_pos) t_visual2link.set_q(geom_rot) geom_radius = geom_size[0] geom_half_length = np.linalg.norm(end - start) * 0.5 else: geom_radius = geom_size[0] geom_half_length = geom_size[1] # TODO (stao): oriented along z-axis for capsules whereas boxes are not? if geom_type in ["capsule", "cylinder"]: t_visual2link = t_visual2link * Pose( q=euler.euler2quat(0, np.pi / 2, 0) ) if geom_type == "capsule": if has_visual_body: builder.add_capsule_visual( t_visual2link, radius=geom_radius, half_length=geom_half_length, material=render_material, name=geom_name, ) if has_collisions: builder.add_capsule_collision( t_visual2link, radius=geom_radius, half_length=geom_half_length, material=physx_material, density=geom_density, # name=geom_name, ) elif geom_type == "box": if has_visual_body: builder.add_box_visual( t_visual2link, half_size=geom_size, material=render_material, name=geom_name, ) if has_collisions: builder.add_box_collision( t_visual2link, half_size=geom_size, material=physx_material, density=geom_density, # name=geom_name, ) elif geom_type == "cylinder": if has_visual_body: builder.add_cylinder_visual( t_visual2link, radius=geom_radius, half_length=geom_half_length, material=render_material, name=geom_name, ) if has_collisions: builder.add_cylinder_collision( t_visual2link, radius=geom_radius, half_length=geom_half_length, material=physx_material, density=geom_density, # name=geom_name ) elif geom_type == "plane": if not WARNED_ONCE["plane"]: logger.warn( "Currently ManiSkill does not support loading plane geometries from MJCFs" ) WARNED_ONCE["plane"] = True elif geom_type == "ellipsoid": if not WARNED_ONCE["ellipsoid"]: logger.warn( "Currently ManiSkill does not support loading ellipsoid geometries from MJCFs" ) WARNED_ONCE["ellipsoid"] = True elif geom_type == "mesh": mesh_name = geom_attrib.get("mesh") mesh_attrib = self._meshes[mesh_name].attrib mesh_scale = self.scale * np.array( _parse_vec(mesh_attrib, "scale", np.array([1, 1, 1])) ) # TODO refquat mesh_file = os.path.join(self._mesh_dir, mesh_attrib["file"]) if has_visual_body: builder.add_visual_from_file( mesh_file, pose=t_visual2link, scale=mesh_scale, material=render_material, ) if has_collisions: if self.load_multiple_collisions_from_file: builder.add_multiple_convex_collisions_from_file( mesh_file, pose=t_visual2link, scale=mesh_scale, material=physx_material, density=geom_density, ) else: builder.add_convex_collision_from_file( mesh_file, pose=t_visual2link, scale=mesh_scale, material=physx_material, density=geom_density, ) elif geom_type == "sdf": raise NotImplementedError("SDF geom type not supported at the moment") elif geom_type == "hfield": raise NotImplementedError("Height fields are not supported at the moment")
[docs] def _parse_texture(self, texture: Element): """Parse MJCF textures to then be referenced by materials: https://mujoco.readthedocs.io/en/stable/XMLreference.html#asset-texture NOTE: - Procedural texture generation is currently not supported. - Different texture types are not really supported """ name = texture.get("name") file = texture.get("file") self._textures[name] = MJCFTexture( name=name, type=texture.get("type"), rgb1=texture.get("rgb1"), rgb2=texture.get("rgb2"), file=os.path.join(self._mesh_dir, file) if file else None, )
[docs] def _parse_material(self, material: Element): """Parse MJCF materials in asset to sapien render materials""" name = material.get("name") texture = None if material.get("texture") in self._textures: texture = self._textures[material.get("texture")] # NOTE: Procedural texture generation is currently not supported. # Defaults from https://mujoco.readthedocs.io/en/stable/XMLreference.html#asset-material em_val = _parse_float(material.attrib, "emission", 0) rgba = np.array(_parse_vec(material.attrib, "rgba", [1, 1, 1, 1])) render_material = RenderMaterial( emission=[rgba[0] * em_val, rgba[1] * em_val, rgba[2] * em_val, 1], base_color=rgba, specular=_parse_float(material.attrib, "specular", 0), # TODO (stao): double check below 2 properties are right roughness=1 - _parse_float(material.attrib, "reflectance", 0), metallic=_parse_float(material.attrib, "shininess", 0.5), ) if texture is not None and texture.file is not None: render_material.base_color_texture = RenderTexture2D(filename=texture.file) self._materials[name] = render_material
[docs] def _parse_mesh(self, mesh: Element): """Parse MJCF mesh data in asset""" # Vertex, normal, texcoord are not supported, file is required file = mesh.get("file") assert ( file is not None ), "Mesh file not provided. While Mujoco allows file to be optional, for loading into SAPIEN this is not optional" name = mesh.get("name", os.path.splitext(file)[0]) self._meshes[name] = mesh
@property
[docs] def _root_default(self): if "__root__" not in self._defaults: return {} return self._defaults["__root__"]
[docs] def _parse_default(self, node: Element, parent: Element): """Parse a MJCF default attribute. https://mujoco.readthedocs.io/en/stable/modeling.html#default-settings explains how it works""" class_name = "__root__" if node.tag == "default": if "class" in node.attrib: class_name = node.attrib["class"] if parent is not None and "class" in parent.attrib: self._defaults[class_name] = deepcopy( self._defaults[parent.attrib["class"]] ) else: self._defaults[class_name] = {} for child in node: if child.tag == "default": self._parse_default(child, node) else: if child.tag in self._defaults[class_name]: self._defaults[class_name][child.tag] = _merge_attrib( self._defaults[class_name][child.tag], child.attrib ) else: self._defaults[class_name][child.tag] = child.attrib
[docs] def _parse_body( self, body: Element, parent: LinkBuilder, incoming_defaults: dict, builder: ArticulationBuilder, ): body_class = body.get("childclass") if body_class is None: defaults = incoming_defaults else: for pattern in self.ignore_classes: if re.match(pattern, body_class): return defaults = _merge_attrib(incoming_defaults, self._defaults[body_class]) if "body" in defaults: body_attrib = _merge_attrib(defaults["body"], body.attrib) else: body_attrib = body.attrib body_name = body_attrib["name"] body_pos = _parse_vec(body_attrib, "pos", (0.0, 0.0, 0.0)) body_ori = _parse_orientation( body_attrib, use_degrees=self._use_degrees, euler_seq=self._euler_seq ) body_pos *= self.scale body_pose = Pose(body_pos, q=body_ori) link_builder = parent joints = body.findall("joint") # if body has no joints, it is a fixed joint if len(joints) == 0: joints = [ET.Element("joint", attrib=dict(type="fixed"))] for i, joint in enumerate(joints): # note there can be multiple joints here. We create some dummy links to simulate that incoming_attributes = [] if "joint" in defaults: incoming_attributes.append(defaults["joint"]) if "class" in joint.attrib: incoming_attributes.append( self._defaults[joint.attrib["class"]]["joint"] ) incoming_attributes.append(joint.attrib) joint_attrib = _merge_attrib(dict(), incoming_attributes) # build the link link_builder = builder.create_link_builder(parent=link_builder) link_builder.set_joint_name(joint_attrib.get("name", "")) if i == len(joints) - 1: link_builder.set_name(f"{body_name}") # the last link is the "real" one, the rest are dummy links to support multiple joints acting on a link self._build_link(body, body_attrib, link_builder, defaults) else: link_builder.set_name(f"{body_name}_dummy_{i}") self._link2builder[link_builder.name] = link_builder joint_type = joint_attrib.get("type", "hinge") joint_pos = np.array(_parse_vec(joint_attrib, "pos", [0, 0, 0])) t_joint2parent = Pose() if i == 0: t_joint2parent = body_pose friction = _parse_float(joint_attrib, "frictionloss", 0) damping = _parse_float(joint_attrib, "damping", 0) # compute joint axis and relative transformations axis = _parse_vec(joint_attrib, "axis", [0.0, 0.0, 0.0]) axis_norm = np.linalg.norm(axis) if axis_norm < 1e-3: axis = np.array([1.0, 0.0, 0.0]) else: axis /= axis_norm if abs(axis @ [1.0, 0.0, 0.0]) > 0.9: axis1 = np.cross(axis, [0.0, 0.0, 1.0]) axis1 /= np.linalg.norm(axis1) else: axis1 = np.cross(axis, [1.0, 0.0, 0.0]) axis1 /= np.linalg.norm(axis1) axis2 = np.cross(axis, axis1) t_axis2joint = np.eye(4) t_axis2joint[:3, 3] = joint_pos t_axis2joint[:3, 0] = axis t_axis2joint[:3, 1] = axis1 t_axis2joint[:3, 2] = axis2 t_axis2joint = Pose(t_axis2joint) t_axis2parent = t_joint2parent * t_axis2joint limited = joint_attrib.get("limited", "auto") if limited == "auto": if "range" in joint_attrib: limited = True else: limited = False elif limited == "true": limited = True else: limited = False # set the joint properties to create it if joint_type == "hinge": if limited: joint_limits = _parse_vec(joint_attrib, "range", [0, 0]) if self._use_degrees: joint_limits = np.deg2rad(joint_limits) link_builder.set_joint_properties( "revolute_unwrapped", [joint_limits], t_axis2parent, t_axis2joint, friction, damping, ) else: link_builder.set_joint_properties( "revolute", [[-np.inf, np.inf]], t_axis2parent, t_axis2joint, friction, damping, ) elif joint_type == "slide": if limited: limits = [_parse_vec(joint_attrib, "range", [0, 0]) * self.scale] else: limits = [[-np.inf, np.inf]] link_builder.set_joint_properties( "prismatic", limits, t_axis2parent, t_axis2joint, friction, damping, ) elif joint_type == "fixed": link_builder.set_joint_properties( "fixed", [], t_axis2parent, t_axis2joint, friction, damping, ) # ensure adjacent links do not collide. Normally SAPIEN does this # but we often create dummy links to support multiple joints between two link functionality # that mujoco has so it must be done here. if parent is not None: parent.collision_groups[2] |= 1 << (self._group_count) link_builder.collision_groups[2] |= 1 << (self._group_count) self._group_count += 1 for child in body.findall("body"): self._parse_body(child, link_builder, defaults, builder)
[docs] def _parse_constraint(self, constraint: Element): joint_elems = [] for joint in constraint.findall("joint"): joint_elems.append(joint) return MimicJointRecord( joint_elems[0].attrib["joint"], joint_elems[1].attrib["joint"], 1, 0 # joint.mimic.multiplier, # joint.mimic.offset, )
[docs] def _parse_mjcf( self, mjcf_string: str ) -> Tuple[list[ArticulationBuilder], list[ActorBuilder], None]: """Helper function for self.parse""" xml: Element = ET.fromstring(mjcf_string.encode("utf-8")) self.xml = xml # handle includes for include in xml.findall("include"): include_file = include.attrib["file"] with open(os.path.join(self.mjcf_dir, include_file), "r") as f: include_file_str = f.read() include_xml = ET.fromstring(include_file_str.encode("utf-8")) for child in include_xml: xml.append(child) self._use_degrees = True # angles are in degrees by default self._mjcf_options = DEFAULT_MJCF_OPTIONS self._euler_seq = [1, 2, 3] # XYZ by default ### Parse compiler options ### compiler = xml.find("compiler") if compiler is not None: self._use_degrees = ( compiler.attrib.get("angle", "degree").lower() == "degree" ) self._euler_seq = [ "xyz".index(c) + 1 for c in compiler.attrib.get("eulerseq", "xyz").lower() ] self._mesh_dir = compiler.attrib.get("meshdir", ".") else: self._mesh_dir = "." self._mesh_dir = os.path.join(self.mjcf_dir, self._mesh_dir) ### Parse options/flags ### option = xml.find("option") if option is not None: for flag in option.findall("flag"): update_dict = dict() for k, v in flag.attrib.items(): update_dict[k] = True if v == "enable" else False self._mjcf_options.update(update_dict) ### Parse assets ### for asset in xml.findall("asset"): for texture in asset.findall("texture"): self._parse_texture(texture) for material in asset.findall("material"): self._parse_material(material) for mesh in asset.findall("mesh"): self._parse_mesh(mesh) ### Parse defaults ### for default in xml.findall("default"): self._parse_default(default, None) ### Parse Kinematic Trees / Articulations in World Body ### # NOTE (stao): For now we assume there is only one articulation. Some setups like Aloha 2 are technically 2 articulations # but you can treat it as a single one anyway articulation_builders: list[ArticulationBuilder] = [] actor_builders: list[ActorBuilder] = [] for i, body in enumerate(xml.find("worldbody").findall("body")): # determine first if this body is really an articulation or a actor has_freejoint = body.find("freejoint") is not None def has_joint(body): if body.find("joint") is not None: return True for child in body.findall("body"): if has_joint(child): return True return False is_articulation = has_joint(body) or has_freejoint # <body> tag refers to an artciulation in physx only if there is another body tag inside it if is_articulation: builder = self.scene.create_articulation_builder() articulation_builders.append(builder) dummy_root_link = builder.create_link_builder(None) dummy_root_link.name = f"dummy_root_{i}" # Check if the body tag only contains another body tag and nothing else body_children = list(body) tag_counts = defaultdict(int) for child in body_children: tag_counts[child.tag] += 1 if ( tag_counts["body"] == 1 and "geom" not in tag_counts and "joint" not in tag_counts ): # If so, skip the current body and continue with its child body = body.find("body") self._parse_body(body, dummy_root_link, self._root_default, builder) # handle free joints fix_root_link = not has_freejoint if fix_root_link: dummy_root_link.set_joint_properties( type="fixed", limits=None, pose_in_parent=Pose(), pose_in_child=Pose(), ) else: builder = self.scene.create_actor_builder() body_type = "dynamic" if has_freejoint else "static" actor_builders.append(builder) # NOTE that mujoco supports nested body tags to define groups of geoms cur_body = body while cur_body is not None: for i, geom in enumerate(cur_body.findall("geom")): self._build_geom(geom, builder, self._root_default) builder.set_name(geom.get("name", "")) builder.set_physx_body_type(body_type) cur_body = cur_body.find("body") ### Parse geoms in World Body ### # These can't have freejoints so they can't be dynamic for i, geom in enumerate(xml.find("worldbody").findall("geom")): builder = self.scene.create_actor_builder() actor_builders.append(builder) self._build_geom(geom, builder, self._root_default) builder.set_name(geom.get("name", "")) builder.set_physx_body_type("static") ### Parse contact and exclusions ### for contact in xml.findall("contact"): # TODO pass ### Parse equality constraints ### # tendon = xml.find("tendon") # if tendon is not None: # # TODO (stao): unclear if this actually works # for constraint in tendon.findall("fixed"): # record = self._parse_constraint(constraint) # builder.mimic_joint_records.append(record) if not self._mjcf_options["contact"]: # means to disable all contacts for actor in actor_builders: actor.collision_groups[2] |= 1 << 1 for art in articulation_builders: for link in art.link_builders: link.collision_groups[2] |= 1 << 1 return articulation_builders, actor_builders, []
[docs] def parse(self, mjcf_file: str, package_dir=None): """Parses a given MJCF file into articulation builders and actor builders and sensor configs""" self.package_dir = package_dir self.mjcf_dir = os.path.dirname(mjcf_file) with open(mjcf_file, "r") as f: mjcf_string = f.read() return self._parse_mjcf(mjcf_string)
[docs] def load(self, mjcf_file: str, package_dir=None): """Parses a given mjcf .xml file and builds all articulations and actors""" articulation_builders, actor_builders, cameras = self.parse( mjcf_file, package_dir ) articulations: list[PhysxArticulation] = [] for b in articulation_builders: articulations.append(b.build()) actors = [] for b in actor_builders: actors.append(b.build()) # TODO (stao): how does mjcf specify sensors? # name2entity = dict() # for a in articulations: # for l in a.links: # name2entity[l.name] = l.entity # for a in actors: # name2entity[a.name] = a return articulations[0]
# TODO (stao): function to also load the scene in? # TODO (stao): function to load camera configs?