From 39d142baba5c2a3ec5b820ddf8537c16e930bb1f Mon Sep 17 00:00:00 2001 From: cremebrule <84cremebrule@gmail.com> Date: Sun, 17 Mar 2024 00:44:57 -0700 Subject: [PATCH 1/2] changes from sampling branch --- omnigibson/macros.py | 6 + omnigibson/object_states/attached_to.py | 178 ++++++++-- omnigibson/object_states/filled.py | 8 +- omnigibson/object_states/folded.py | 10 +- omnigibson/object_states/inside.py | 6 +- omnigibson/object_states/on_top.py | 6 +- omnigibson/object_states/under.py | 6 +- omnigibson/objects/controllable_object.py | 8 +- omnigibson/objects/dataset_object.py | 4 + omnigibson/prims/entity_prim.py | 17 + omnigibson/scenes/scene_base.py | 11 +- omnigibson/simulator.py | 10 +- omnigibson/systems/macro_particle_system.py | 21 +- omnigibson/systems/micro_particle_system.py | 28 +- omnigibson/systems/system_base.py | 26 +- omnigibson/transition_rules.py | 1 + omnigibson/utils/asset_utils.py | 28 ++ omnigibson/utils/bddl_utils.py | 364 ++++++++++++++++---- omnigibson/utils/usd_utils.py | 13 +- 19 files changed, 620 insertions(+), 131 deletions(-) diff --git a/omnigibson/macros.py b/omnigibson/macros.py index 4d5f93738..ad5359dd3 100644 --- a/omnigibson/macros.py +++ b/omnigibson/macros.py @@ -88,6 +88,12 @@ # Maximum particle contacts allowed gm.GPU_MAX_PARTICLE_CONTACTS = 1024 * 1024 +# Maximum rigid contacts -- 524288 is default value from omni, but increasing too much can sometimes lead to crashes +gm.GPU_MAX_RIGID_CONTACT_COUNT = 524288 * 4 + +# Maximum rigid patches -- 81920 is default value from omni, but increasing too much can sometimes lead to crashes +gm.GPU_MAX_RIGID_PATCH_COUNT = 81920 * 4 + # Whether to enable object state logic or not gm.ENABLE_OBJECT_STATES = True diff --git a/omnigibson/object_states/attached_to.py b/omnigibson/object_states/attached_to.py index 46abc5a63..dfd675ebb 100644 --- a/omnigibson/object_states/attached_to.py +++ b/omnigibson/object_states/attached_to.py @@ -14,6 +14,7 @@ from omnigibson.utils.usd_utils import create_joint from omnigibson.utils.ui_utils import create_module_logger from omnigibson.utils.python_utils import classproperty +from omnigibson.utils.usd_utils import CollisionAPI # Create module logger log = create_module_logger(module_name=__name__) @@ -30,7 +31,15 @@ m.DEFAULT_BREAK_FORCE = 1000 # Newton m.DEFAULT_BREAK_TORQUE = 1000 # Newton-Meter - +# TODO: Make AttachedTo into a global state that manages all the attachments in the scene. +# When an attachment of a child and a parent is about to happen: +# 1. stop the sim +# 2. remove all existing attachment joints (and save information to restore later) +# 3. disable collision between the child and the parent +# 4. play the sim +# 5. reload the state +# 6. restore all existing attachment joints +# 7. create the joint class AttachedTo(RelativeObjectState, BooleanStateMixin, ContactSubscribedStateMixin, JointBreakSubscribedStateMixin, LinkBasedStateMixin): """ Handles attachment between two rigid objects, by creating a fixed/spherical joint between self.obj (child) and @@ -43,6 +52,20 @@ class AttachedTo(RelativeObjectState, BooleanStateMixin, ContactSubscribedStateM on_contact function attempts to attach self.obj to other when a CONTACT_FOUND event happens on_joint_break function breaks the current attachment """ + # This is to force the __init__ args to be "self" and "obj" only. + # Otherwise, it will inherit from LinkBasedStateMixin and the __init__ args will be "self", "args", "kwargs". + def __init__(self, obj): + # Run super method + super().__init__(obj=obj) + + def initialize(self): + super().initialize() + og.sim.add_callback_on_stop(name=f"{self.obj.name}_detach", callback=self._detach) + self.parents_disabled_collisions = set() + + def remove(self): + super().remove() + og.sim.remove_callback_on_stop(name=f"{self.obj.name}_detach") @classproperty def metalink_prefix(cls): @@ -89,24 +112,54 @@ def on_contact(self, other, contact_headers, contact_data): if self.set_value(other, True): break - def _set_value(self, other, new_value, bypass_alignment_checking=False): + def _set_value(self, other, new_value, bypass_alignment_checking=False, check_physics_stability=False, can_joint_break=True): + """ + Args: + other (DatasetObject): parent object to attach to. + new_value (bool): whether to attach or detach. + bypass_alignment_checking (bool): whether to bypass alignment checking when finding attachment links. + Normally when finding attachment links, we check if the child and parent links have aligned positions + or poses. This flag allows users to bypass this check and find attachment links solely based on the + attachment meta link types. Default is False. + check_physics_stability (bool): whether to check if the attachment is stable after attachment. + If True, it will check if the child object is not colliding with other objects except the parent object. + If False, it will not check the stability and simply attach the child to the parent. + Default is False. + can_joint_break (bool): whether the joint can break or not. + + Returns: + bool: whether the attachment setting was successful or not. + """ # Attempt to attach if new_value: if self.parent == other: # Already attached to this object. Do nothing. return True - elif self.parent is None: - # Find attachment links that satisfy the proximity requirements - child_link, parent_link = self._find_attachment_links(other, bypass_alignment_checking) - if child_link is not None: - self._attach(other, child_link, parent_link) - return True - else: - return False - else: + elif self.parent is not None: log.debug(f"Trying to attach object {self.obj.name} to object {other.name}," f"but it is already attached to object {self.parent.name}. Try detaching first.") return False + else: + # Find attachment links that satisfy the proximity requirements + child_link, parent_link = self._find_attachment_links(other, bypass_alignment_checking) + if child_link is None: + return False + else: + if check_physics_stability: + state = og.sim.dump_state() + self._attach(other, child_link, parent_link, can_joint_break=can_joint_break) + if not check_physics_stability: + return True + else: + og.sim.step_physics() + # self.obj should not collide with other objects except the parent + success = len(self.obj.states[ContactBodies].get_value(ignore_objs=(other,))) == 0 + if success: + return True + else: + self._detach() + og.sim.load_state(state) + return False # Attempt to detach else: @@ -190,7 +243,7 @@ def _get_parent_candidates(self, other): def attachment_joint_prim_path(self): return f"{self.parent_link.prim_path}/{self.obj.name}_attachment_joint" if self.parent_link is not None else None - def _attach(self, other, child_link, parent_link, joint_type=m.DEFAULT_JOINT_TYPE, break_force=m.DEFAULT_BREAK_FORCE, break_torque=m.DEFAULT_BREAK_TORQUE): + def _attach(self, other, child_link, parent_link, joint_type=m.DEFAULT_JOINT_TYPE, can_joint_break=True): """ Creates a fixed or spherical joint between a male meta link of self.obj (@child_link) and a female meta link of @other (@parent_link) with a given @joint_type, @break_force and @break_torque @@ -200,16 +253,9 @@ def _attach(self, other, child_link, parent_link, joint_type=m.DEFAULT_JOINT_TYP child_link (RigidPrim): male meta link of @self.obj. parent_link (RigidPrim): female meta link of @other. joint_type (JointType): joint type of the attachment, {JointType.JOINT_FIXED, JointType.JOINT_SPHERICAL} - break_force (float or None): break force for linear dofs, unit is Newton. - break_torque (float or None): break torque for angular dofs, unit is Newton-meter. + can_joint_break (bool): whether the joint can break or not. """ assert joint_type in {JointType.JOINT_FIXED, JointType.JOINT_SPHERICAL}, f"Unsupported joint type {joint_type}" - # Set the parent references - self.parent = other - self.parent_link = parent_link - - # Set the child reference for @other - other.states[AttachedTo].children[parent_link.body_name] = self.obj # Set pose for self.obj so that child_link and parent_link align (6dof alignment for FixedJoint and 3dof alignment for SphericalJoint) parent_pos, parent_quat = parent_link.get_position_orientation() @@ -229,6 +275,7 @@ def _attach(self, other, child_link, parent_link, joint_type=m.DEFAULT_JOINT_TYP # Actually move the object and also keep it still for stability purposes. self.obj.set_position_orientation(new_child_root_pos, new_child_root_quat) self.obj.keep_still() + other.keep_still() if joint_type == JointType.JOINT_FIXED: # FixedJoint: the parent link, the child link and the joint frame all align. @@ -238,6 +285,18 @@ def _attach(self, other, child_link, parent_link, joint_type=m.DEFAULT_JOINT_TYP # The child link and the joint frame still align. _, parent_local_quat = T.relative_pose_transform([0, 0, 0], child_quat, [0, 0, 0], parent_quat) + # Disable collision between the parent and child objects + self._disable_collision_between_child_and_parent(child=self.obj, parent=other) + + # Set the parent references + self.parent = other + self.parent_link = parent_link + + # Set the child reference for @other + other.states[AttachedTo].children[parent_link.body_name] = self.obj + + kwargs = {"break_force": m.DEFAULT_BREAK_FORCE, "break_torque": m.DEFAULT_BREAK_TORQUE} if can_joint_break else dict() + # Create the joint create_joint( prim_path=self.attachment_joint_prim_path, @@ -248,23 +307,61 @@ def _attach(self, other, child_link, parent_link, joint_type=m.DEFAULT_JOINT_TYP joint_frame_in_parent_frame_quat=parent_local_quat, joint_frame_in_child_frame_pos=np.zeros(3), joint_frame_in_child_frame_quat=np.array([0.0, 0.0, 0.0, 1.0]), - break_force=break_force, - break_torque=break_torque, + **kwargs ) + def _disable_collision_between_child_and_parent(self, child, parent): + """ + Disables collision between the child and parent objects + """ + if parent in self.parents_disabled_collisions: + return + self.parents_disabled_collisions.add(parent) + + was_playing = og.sim.is_playing() + if was_playing: + state = og.sim.dump_state() + og.sim.stop() + + for child_link in child.links.values(): + for parent_link in parent.links.values(): + child_link.add_filtered_collision_pair(parent_link) + + if parent.category == "wall_nail": + # Temporary hack to disable collision between the attached child object and all walls/floors so that objects + # attached to the wall_nails do not collide with the walls/floors. + for wall in og.sim.scene.object_registry("category", "walls", set()): + for wall_link in wall.links.values(): + for child_link in child.links.values(): + child_link.add_filtered_collision_pair(wall_link) + for wall in og.sim.scene.object_registry("category", "floors", set()): + for floor_link in wall.links.values(): + for child_link in child.links.values(): + child_link.add_filtered_collision_pair(floor_link) + + # Temporary hack to disable gravity for the attached child object if the parent is kinematic_only + # Otherwise, the parent metalink will oscillate due to the gravity force of the child. + if parent.kinematic_only: + child.disable_gravity() + + if was_playing: + og.sim.play() + og.sim.load_state(state) + def _detach(self): """ Removes the current attachment joint """ - # Remove the attachment joint prim from the stage - og.sim.stage.RemovePrim(self.attachment_joint_prim_path) + if self.parent_link is not None: + # Remove the attachment joint prim from the stage + og.sim.stage.RemovePrim(self.attachment_joint_prim_path) - # Remove child reference from the parent object - self.parent.states[AttachedTo].children[self.parent_link.body_name] = None + # Remove child reference from the parent object + self.parent.states[AttachedTo].children[self.parent_link.body_name] = None - # Remove reference to the parent object and link - self.parent = None - self.parent_link = None + # Remove reference to the parent object and link + self.parent = None + self.parent_link = None @property def settable(self): @@ -285,15 +382,20 @@ def _load_state(self, state): attached_obj = og.sim.scene.object_registry("uuid", uuid) assert attached_obj is not None, "attached_obj_uuid does not match any object in the scene." - # If it's currently attached to something, detach. - if self.parent is not None: - self.set_value(self.parent, False) - assert self.parent is None, "parent reference is not cleared after detachment" - - # If the loaded state requires attachment, attach. - if attached_obj is not None: - self.set_value(attached_obj, True) - assert self.parent == attached_obj, "parent reference is not updated after attachment" + if self.parent != attached_obj: + # If it's currently attached to something else, detach. + if self.parent is not None: + self.set_value(self.parent, False) + # assert self.parent is None, "parent reference is not cleared after detachment" + if self.parent is not None: + log.warning(f"parent reference is not cleared after detachment") + + # If the loaded state requires attachment, attach. + if attached_obj is not None: + self.set_value(attached_obj, True, bypass_alignment_checking=True, check_physics_stability=False, can_joint_break=True) + # assert self.parent == attached_obj, "parent reference is not updated after attachment" + if self.parent != attached_obj: + log.warning(f"parent reference is not updated after attachment") def _serialize(self, state): return np.array([state["attached_obj_uuid"]], dtype=float) diff --git a/omnigibson/object_states/filled.py b/omnigibson/object_states/filled.py index e3f4b50fc..380429965 100644 --- a/omnigibson/object_states/filled.py +++ b/omnigibson/object_states/filled.py @@ -3,12 +3,15 @@ from omnigibson.object_states.contains import ContainedParticles from omnigibson.object_states.object_state_base import RelativeObjectState, BooleanStateMixin from omnigibson.systems.system_base import PhysicalParticleSystem, is_physical_particle_system +from omnigibson.systems.macro_particle_system import MacroParticleSystem # Create settings for this module m = create_module_macros(module_path=__file__) # Proportion of object's volume that must be filled for object to be considered filled m.VOLUME_FILL_PROPORTION = 0.2 +m.N_MAX_MACRO_PARTICLE_SAMPLES = 500 +m.N_MAX_MICRO_PARTICLE_SAMPLES = 100000 class Filled(RelativeObjectState, BooleanStateMixin): @@ -20,7 +23,8 @@ def _get_value(self, system): # Check what volume is filled if system.n_particles > 0: - particle_volume = 4 / 3 * np.pi * (system.particle_radius ** 3) + # Treat particles as cubes + particle_volume = (system.particle_radius * 2) ** 3 n_particles = self.obj.states[ContainedParticles].get_value(system).n_in_volume prop_filled = particle_volume * n_particles / self.obj.states[ContainedParticles].volume # If greater than threshold, then the volume is filled @@ -50,6 +54,8 @@ def _set_value(self, system, new_value): obj=self.obj, link=contained_particles_state.link, check_contact=True, + max_samples=m.N_MAX_MACRO_PARTICLE_SAMPLES + if issubclass(system, MacroParticleSystem) else m.N_MAX_MICRO_PARTICLE_SAMPLES ) else: # Cannot set False diff --git a/omnigibson/object_states/folded.py b/omnigibson/object_states/folded.py index 1414f15cb..9fd8ff428 100644 --- a/omnigibson/object_states/folded.py +++ b/omnigibson/object_states/folded.py @@ -1,6 +1,6 @@ import numpy as np from collections import namedtuple -from scipy.spatial import ConvexHull, distance_matrix +from scipy.spatial import ConvexHull, distance_matrix, QhullError from omnigibson.macros import create_module_macros from omnigibson.object_states.object_state_base import BooleanStateMixin, AbsoluteObjectState @@ -98,7 +98,13 @@ def calculate_projection_area_and_diagonal(self, dims): """ cloth = self.obj.root_link points = cloth.keypoint_particle_positions[:, dims] - hull = ConvexHull(points) + try: + hull = ConvexHull(points) + + # The points may be 2D-degenerate, so catch the error and return 0 if so + except QhullError: + # This is a degenerate hull, so return 0 area and diagonal + return 0.0, 0.0 # When input points are 2-dimensional, this is the area of the convex hull. # Ref: https://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.ConvexHull.html diff --git a/omnigibson/object_states/inside.py b/omnigibson/object_states/inside.py index ac5188ae5..7d5f8e44f 100644 --- a/omnigibson/object_states/inside.py +++ b/omnigibson/object_states/inside.py @@ -16,7 +16,7 @@ def get_dependencies(cls): deps.update({AABB, HorizontalAdjacency, VerticalAdjacency}) return deps - def _set_value(self, other, new_value): + def _set_value(self, other, new_value, reset_before_sampling=False): if not new_value: raise NotImplementedError("Inside does not support set_value(False)") @@ -25,6 +25,10 @@ def _set_value(self, other, new_value): state = og.sim.dump_state(serialized=False) + # Possibly reset this object if requested + if reset_before_sampling: + self.obj.reset() + for _ in range(os_m.DEFAULT_HIGH_LEVEL_SAMPLING_ATTEMPTS): if sample_kinematics("inside", self.obj, other) and self.get_value(other): return True diff --git a/omnigibson/object_states/on_top.py b/omnigibson/object_states/on_top.py index d1dd8e438..2b38ed5d5 100644 --- a/omnigibson/object_states/on_top.py +++ b/omnigibson/object_states/on_top.py @@ -16,7 +16,7 @@ def get_dependencies(cls): deps.update({Touching, VerticalAdjacency}) return deps - def _set_value(self, other, new_value): + def _set_value(self, other, new_value, reset_before_sampling=False): if not new_value: raise NotImplementedError("OnTop does not support set_value(False)") @@ -25,6 +25,10 @@ def _set_value(self, other, new_value): state = og.sim.dump_state(serialized=False) + # Possibly reset this object if requested + if reset_before_sampling: + self.obj.reset() + for _ in range(os_m.DEFAULT_HIGH_LEVEL_SAMPLING_ATTEMPTS): if sample_kinematics("onTop", self.obj, other) and self.get_value(other): return True diff --git a/omnigibson/object_states/under.py b/omnigibson/object_states/under.py index 78f205b7d..a9858d14b 100644 --- a/omnigibson/object_states/under.py +++ b/omnigibson/object_states/under.py @@ -14,7 +14,7 @@ def get_dependencies(cls): deps.add(VerticalAdjacency) return deps - def _set_value(self, other, new_value): + def _set_value(self, other, new_value, reset_before_sampling=False): if not new_value: raise NotImplementedError("Under does not support set_value(False)") @@ -23,6 +23,10 @@ def _set_value(self, other, new_value): state = og.sim.dump_state(serialized=False) + # Possibly reset this object if requested + if reset_before_sampling: + self.obj.reset() + for _ in range(os_m.DEFAULT_HIGH_LEVEL_SAMPLING_ATTEMPTS): if sample_kinematics("under", self.obj, other) and self.get_value(other): return True diff --git a/omnigibson/objects/controllable_object.py b/omnigibson/objects/controllable_object.py index f51fc6124..60301dd13 100644 --- a/omnigibson/objects/controllable_object.py +++ b/omnigibson/objects/controllable_object.py @@ -248,13 +248,11 @@ def reload_controllers(self, controller_config=None): else self._create_continuous_action_space() def reset(self): - # Make sure simulation is playing, otherwise, we cannot reset because physx requires active running - # simulation in order to set joints - assert og.sim.is_playing(), "Simulator must be playing in order to reset controllable object's joints!" + # Call super first + super().reset() - # Additionally set the joint states based on the reset values + # Override the reset joint state based on reset values self.set_joint_positions(positions=self._reset_joint_pos, drive=False) - self.set_joint_velocities(velocities=np.zeros(self.n_dof), drive=False) @abstractmethod def _create_discrete_action_space(self): diff --git a/omnigibson/objects/dataset_object.py b/omnigibson/objects/dataset_object.py index 551e15da7..b5c59d124 100644 --- a/omnigibson/objects/dataset_object.py +++ b/omnigibson/objects/dataset_object.py @@ -104,6 +104,10 @@ def __init__( load_config = dict() if load_config is None else load_config load_config["bounding_box"] = bounding_box + # TODO: Remove once meshes are fixed + from omnigibson.utils.bddl_utils import DO_NOT_REMESH_CLOTHS + load_config["remesh"] = model not in DO_NOT_REMESH_CLOTHS.get(category, set()) + # Infer the correct usd path to use if model is None: available_models = get_all_object_category_models(category=category) diff --git a/omnigibson/prims/entity_prim.py b/omnigibson/prims/entity_prim.py index 71bf6bee0..90a8276d5 100644 --- a/omnigibson/prims/entity_prim.py +++ b/omnigibson/prims/entity_prim.py @@ -608,6 +608,23 @@ def disable_gravity(self) -> None: for link in self._links.values(): link.disable_gravity() + def reset(self): + """ + Resets this entity to some default, pre-defined state + """ + # Make sure simulation is playing, otherwise, we cannot reset because physx requires active running + # simulation in order to set joints + assert og.sim.is_playing(), "Simulator must be playing in order to reset controllable object's joints!" + + # If this is a cloth, reset the particle positions + if self.prim_type == PrimType.CLOTH: + self.root_link.reset() + + # Otherwise, set all joints to have 0 position and 0 velocity if this object has joints + elif self.n_joints > 0: + self.set_joint_positions(positions=np.zeros(self.n_dof), drive=False) + self.set_joint_velocities(velocities=np.zeros(self.n_dof), drive=False) + def set_joint_positions(self, positions, indices=None, normalized=False, drive=False): """ Set the joint positions (both actual value and target values) in simulation. Note: only works if the simulator diff --git a/omnigibson/scenes/scene_base.py b/omnigibson/scenes/scene_base.py index 6d196f735..04ceb3f03 100644 --- a/omnigibson/scenes/scene_base.py +++ b/omnigibson/scenes/scene_base.py @@ -32,6 +32,9 @@ # Global dicts that will contain mappings REGISTERED_SCENES = dict() +BOUNDING_CUBE_OBJECTS = { + "xbfgjc": {"base_link"}, +} class Scene(Serializable, Registerable, Recreatable, ABC): """ @@ -181,7 +184,7 @@ def _load(self): # Disable collision between building structures CollisionAPI.create_collision_group(col_group="structures", filter_self_collisions=True) - # Disable collision between building structures and fixed base objects + # Disable collision between building structures and 1. fixed base objects, 2. attached objects CollisionAPI.add_group_filter(col_group="structures", filter_group="fixed_base_nonroot_links") CollisionAPI.add_group_filter(col_group="structures", filter_group="fixed_base_root_links") @@ -237,6 +240,12 @@ def _load_objects_from_scene_file(self): obj = create_object_from_init_info(obj_info) # Import into the simulator og.sim.import_object(obj) + if isinstance(obj, DatasetObject) and obj.model in BOUNDING_CUBE_OBJECTS: + link_names = BOUNDING_CUBE_OBJECTS[obj.model] + for link_name in link_names: + link = obj.links[link_name] + for col_mesh in link.collision_meshes.values(): + col_mesh.set_collision_approximation("boundingCube") # Set the init pose accordingly obj.set_position_orientation( position=init_state[obj_name]["root_link"]["pos"], diff --git a/omnigibson/simulator.py b/omnigibson/simulator.py index 0d11509ae..3e99d64ff 100644 --- a/omnigibson/simulator.py +++ b/omnigibson/simulator.py @@ -331,6 +331,8 @@ def _set_physics_engine_settings(self): self._physics_context.set_gpu_found_lost_aggregate_pairs_capacity(gm.GPU_AGGR_PAIRS_CAPACITY) self._physics_context.set_gpu_total_aggregate_pairs_capacity(gm.GPU_AGGR_PAIRS_CAPACITY) self._physics_context.set_gpu_max_particle_contacts(gm.GPU_MAX_PARTICLE_CONTACTS) + self._physics_context.set_gpu_max_rigid_contact_count(gm.GPU_MAX_RIGID_CONTACT_COUNT) + self._physics_context.set_gpu_max_rigid_patch_count(gm.GPU_MAX_RIGID_PATCH_COUNT) def _set_renderer_settings(self): if gm.ENABLE_HQ_RENDERING: @@ -953,7 +955,7 @@ def remove_callback_on_play(self, name): Args: name (str): Name of the callback """ - self._callbacks_on_play.pop(name) + self._callbacks_on_play.pop(name, None) def remove_callback_on_stop(self, name): """ @@ -962,7 +964,7 @@ def remove_callback_on_stop(self, name): Args: name (str): Name of the callback """ - self._callbacks_on_stop.pop(name) + self._callbacks_on_stop.pop(name, None) def remove_callback_on_import_obj(self, name): """ @@ -971,7 +973,7 @@ def remove_callback_on_import_obj(self, name): Args: name (str): Name of the callback """ - self._callbacks_on_import_obj.pop(name) + self._callbacks_on_import_obj.pop(name, None) def remove_callback_on_remove_obj(self, name): """ @@ -980,7 +982,7 @@ def remove_callback_on_remove_obj(self, name): Args: name (str): Name of the callback """ - self._callbacks_on_remove_obj.pop(name) + self._callbacks_on_remove_obj.pop(name, None) @classmethod def clear_instance(cls): diff --git a/omnigibson/systems/macro_particle_system.py b/omnigibson/systems/macro_particle_system.py index 7518fa24a..6bb131da9 100644 --- a/omnigibson/systems/macro_particle_system.py +++ b/omnigibson/systems/macro_particle_system.py @@ -20,6 +20,11 @@ # Create module logger log = create_module_logger(module_name=__name__) +# Create settings for this module +m = create_module_macros(module_path=__file__) + +m.MIN_PARTICLE_RADIUS = 0.01 # Minimum particle radius for physical macro particles -- this reduces the chance of omni physx crashing + class MacroParticleSystem(BaseSystem): """ @@ -527,7 +532,9 @@ def generate_group_particles( cls.set_particle_position_orientation(idx=-1, position=position, orientation=orientation) @classmethod - def generate_group_particles_on_object(cls, group, max_samples, min_samples_for_success=1): + def generate_group_particles_on_object(cls, group, max_samples=None, min_samples_for_success=1): + # This function does not support max_samples=None. Must be explicitly specified + assert max_samples is not None, f"max_samples must be specified for {cls.name}'s generate_group_particles_on_object!" assert max_samples >= min_samples_for_success, "number of particles to sample should exceed the min for success" # Make sure the group exists @@ -1173,7 +1180,17 @@ def process_particle_object(cls): # Compute particle radius vertices = np.array(cls.particle_object.get_attribute("points")) * cls.particle_object.scale * cls.max_scale.reshape(1, 3) - cls._particle_offset, cls._particle_radius = trimesh.nsphere.minimum_nsphere(trimesh.Trimesh(vertices=vertices)) + + particle_offset, particle_radius = trimesh.nsphere.minimum_nsphere(trimesh.Trimesh(vertices=vertices)) + + if particle_radius < m.MIN_PARTICLE_RADIUS: + ratio = m.MIN_PARTICLE_RADIUS / particle_radius + cls.particle_object.scale *= ratio + particle_offset *= ratio + particle_radius = m.MIN_PARTICLE_RADIUS + + cls._particle_offset = particle_offset + cls._particle_radius = particle_radius @classmethod def refresh_particles_view(cls): diff --git a/omnigibson/systems/micro_particle_system.py b/omnigibson/systems/micro_particle_system.py index 675c14c3c..ac4c84880 100644 --- a/omnigibson/systems/micro_particle_system.py +++ b/omnigibson/systems/micro_particle_system.py @@ -41,6 +41,9 @@ m.CLOTH_FRICTION = 0.4 m.CLOTH_DRAG = 0.001 m.CLOTH_LIFT = 0.003 +m.MIN_PARTICLE_CONTACT_OFFSET = 0.005 # Minimum particle contact offset for physical micro particles +m.FLUID_PARTICLE_PARTICLE_DISTANCE_SCALE = 0.8 # How much overlap expected between fluid particles at rest +m.MICRO_PARTICLE_SYSTEM_MAX_VELOCITY = None # If set, the maximum particle velocity for micro particle systems def set_carb_settings_for_fluid_isosurface(): @@ -633,6 +636,10 @@ def initialize(cls): # Run super super().initialize() + # Potentially set system prim's max velocity value + if m.MICRO_PARTICLE_SYSTEM_MAX_VELOCITY is not None: + cls.system_prim.GetProperty("maxVelocity").Set(m.MICRO_PARTICLE_SYSTEM_MAX_VELOCITY) + # Initialize class variables that are mutable so they don't get overridden by children classes cls.particle_instancers = dict() @@ -902,7 +909,7 @@ def generate_particles_from_link( instancer_idn=None, particle_group=0, sampling_distance=None, - max_samples=5e5, + max_samples=None, prototype_indices=None, ): """ @@ -929,7 +936,7 @@ def generate_particles_from_link( Only used if a new particle instancer is created! sampling_distance (None or float): If specified, sets the distance between sampled particles. If None, a simulator autocomputed value will be used - max_samples (int): Maximum number of particles to sample + max_samples (None or int): If specified, maximum number of particles to sample prototype_indices (None or list of int): If specified, should specify which prototype should be used for each particle. If None, will randomly sample from all available prototypes """ @@ -953,7 +960,7 @@ def generate_particles_on_object( instancer_idn=None, particle_group=0, sampling_distance=None, - max_samples=5e5, + max_samples=None, min_samples_for_success=1, prototype_indices=None, ): @@ -972,7 +979,7 @@ def generate_particles_on_object( Only used if a new particle instancer is created! sampling_distance (None or float): If specified, sets the distance between sampled particles. If None, a simulator autocomputed value will be used - max_samples (int): Maximum number of particles to sample + max_samples (None or int): If specified, maximum number of particles to sample min_samples_for_success (int): Minimum number of particles required to be sampled successfully in order for this generation process to be considered successful prototype_indices (None or list of int): If specified, should specify which prototype should be used for @@ -1296,6 +1303,11 @@ def particle_radius(cls): # See https://docs.omniverse.nvidia.com/prod_extensions/prod_extensions/ext_physics.html#offset-autocomputation return 0.99 * 0.6 * cls.particle_contact_offset + @classproperty + def particle_particle_rest_distance(cls): + # Magic number, based on intuition from https://docs.omniverse.nvidia.com/extensions/latest/ext_physics/physics-particles.html#particle-particle-interaction + return cls.particle_radius * 2.0 * m.FLUID_PARTICLE_PARTICLE_DISTANCE_SCALE + @classproperty def _material_mtl_name(cls): """ @@ -1471,8 +1483,14 @@ def _create_particle_prototypes(cls): ) # Store the contact offset based on a minimum sphere + # Threshold the lower-bound to avoid super small particles vertices = np.array(prototype.get_attribute("points")) * prototype.scale - _, cls._particle_contact_offset = trimesh.nsphere.minimum_nsphere(trimesh.Trimesh(vertices=vertices)) + _, particle_contact_offset = trimesh.nsphere.minimum_nsphere(trimesh.Trimesh(vertices=vertices)) + if particle_contact_offset < m.MIN_PARTICLE_CONTACT_OFFSET: + prototype.scale *= m.MIN_PARTICLE_CONTACT_OFFSET / particle_contact_offset + particle_contact_offset = m.MIN_PARTICLE_CONTACT_OFFSET + + cls._particle_contact_offset = particle_contact_offset return [prototype] diff --git a/omnigibson/systems/system_base.py b/omnigibson/systems/system_base.py index 80d64fa18..a5b73dcbf 100644 --- a/omnigibson/systems/system_base.py +++ b/omnigibson/systems/system_base.py @@ -755,14 +755,14 @@ def generate_group_particles( raise NotImplementedError @classmethod - def generate_group_particles_on_object(cls, group, max_samples, min_samples_for_success=1): + def generate_group_particles_on_object(cls, group, max_samples=None, min_samples_for_success=1): """ Generates @max_samples new particle objects and samples their locations on the surface of object @obj. Note that if any particles are in the group already, they will be removed Args: group (str): Object on which to sample particle locations - max_samples (int): Maximum number of particles to sample + max_samples (None or int): If specified, maximum number of particles to sample min_samples_for_success (int): Minimum number of particles required to be sampled successfully in order for this generation process to be considered successful @@ -880,6 +880,14 @@ def particle_contact_radius(cls): """ raise NotImplementedError() + @classproperty + def particle_particle_rest_distance(cls): + """ + Returns: + The minimum distance between individual particles at rest + """ + return cls.particle_radius * 2.0 + @classmethod def check_in_contact(cls, positions): """ @@ -913,7 +921,7 @@ def generate_particles_from_link( mesh_name_prefixes=None, check_contact=True, sampling_distance=None, - max_samples=5e5, + max_samples=None, **kwargs, ): """ @@ -931,7 +939,7 @@ def generate_particles_from_link( check_contact (bool): If True, will only spawn in particles that do not collide with other rigid bodies sampling_distance (None or float): If specified, sets the distance between sampled particles. If None, a simulator autocomputed value will be used - max_samples (int): Maximum number of particles to sample + max_samples (None or int): If specified, maximum number of particles to sample **kwargs (dict): Any additional keyword-mapped arguments required by subclass implementation """ # Run sanity checks @@ -959,7 +967,7 @@ def generate_particles_from_link( assert np.all(n_particles_per_axis), f"link {link.name} is too small to sample any particle of radius {cls.particle_radius}." # 1e-10 is added because the extent might be an exact multiple of particle radius - arrs = [np.arange(l + cls.particle_radius, h - cls.particle_radius + 1e-10, cls.particle_radius * 2) + arrs = [np.arange(l + cls.particle_radius, h - cls.particle_radius + 1e-10, cls.particle_particle_rest_distance) for l, h, n in zip(low, high, n_particles_per_axis)] # Generate 3D-rectangular grid of points particle_positions = np.stack([arr.flatten() for arr in np.meshgrid(*arrs)]).T @@ -971,7 +979,7 @@ def generate_particles_from_link( particle_positions = particle_positions[np.where(cls.check_in_contact(particle_positions) == 0)[0]] # Also potentially sub-sample if we're past our limit - if len(particle_positions) > max_samples: + if max_samples is not None and len(particle_positions) > max_samples: particle_positions = particle_positions[ np.random.choice(len(particle_positions), size=(int(max_samples),), replace=False)] @@ -985,7 +993,7 @@ def generate_particles_on_object( cls, obj, sampling_distance=None, - max_samples=5e5, + max_samples=None, min_samples_for_success=1, **kwargs, ): @@ -997,7 +1005,7 @@ def generate_particles_on_object( top surface sampling_distance (None or float): If specified, sets the distance between sampled particles. If None, a simulator autocomputed value will be used - max_samples (int): Maximum number of particles to sample + max_samples (None or int): If specified, maximum number of particles to sample min_samples_for_success (int): Minimum number of particles required to be sampled successfully in order for this generation process to be considered successful **kwargs (dict): Any additional keyword-mapped arguments required by subclass implementation @@ -1025,7 +1033,7 @@ def generate_particles_on_object( ) particle_positions = np.array([result[0] for result in results if result[0] is not None]) # Also potentially sub-sample if we're past our limit - if len(particle_positions) > max_samples: + if max_samples is not None and len(particle_positions) > max_samples: particle_positions = particle_positions[ np.random.choice(len(particle_positions), size=(max_samples,), replace=False)] diff --git a/omnigibson/transition_rules.py b/omnigibson/transition_rules.py index c5ebf2fea..9f223d741 100644 --- a/omnigibson/transition_rules.py +++ b/omnigibson/transition_rules.py @@ -1976,6 +1976,7 @@ def candidate_filters(cls): # Exclude washer and clothes dryer because they are handled by WasherRule and DryerRule NotFilter(CategoryFilter("washer")), NotFilter(CategoryFilter("clothes_dryer")), + NotFilter(CategoryFilter("hot_tub")), ]) return candidate_filters diff --git a/omnigibson/utils/asset_utils.py b/omnigibson/utils/asset_utils.py index ca11e6fdf..d98e277e8 100644 --- a/omnigibson/utils/asset_utils.py +++ b/omnigibson/utils/asset_utils.py @@ -286,6 +286,34 @@ def supports_abilities(info, obj_prim): return valid_models +def get_attachment_metalinks(category, model): + """ + Get attachment metalinks for an object model + + Args: + category (str): Object category name + model (str): Object model name + + Returns: + list of str: all attachment metalinks for the object model + """ + # Avoid circular imports + from omnigibson.objects.dataset_object import DatasetObject + from omnigibson.object_states import AttachedTo + + usd_path = DatasetObject.get_usd_path(category=category, model=model) + usd_path = usd_path.replace(".usd", ".encrypted.usd") + with decrypted(usd_path) as fpath: + stage = lazy.pxr.Usd.Stage.Open(fpath) + prim = stage.GetDefaultPrim() + attachment_metalinks = [] + for child in prim.GetChildren(): + if child.GetTypeName() == "Xform": + if AttachedTo.metalink_prefix in child.GetName(): + attachment_metalinks.append(child.GetName()) + return attachment_metalinks + + def get_og_assets_version(): """ Returns: diff --git a/omnigibson/utils/bddl_utils.py b/omnigibson/utils/bddl_utils.py index d664da08f..31f61570b 100644 --- a/omnigibson/utils/bddl_utils.py +++ b/omnigibson/utils/bddl_utils.py @@ -1,6 +1,7 @@ import json import bddl import os +import random import numpy as np import networkx as nx from collections import defaultdict @@ -16,7 +17,7 @@ import omnigibson as og from omnigibson.macros import gm, create_module_macros from omnigibson.utils.constants import PrimType -from omnigibson.utils.asset_utils import get_all_object_categories, get_all_object_category_models_with_abilities +from omnigibson.utils.asset_utils import get_attachment_metalinks, get_all_object_categories, get_all_object_category_models_with_abilities from omnigibson.utils.ui_utils import create_module_logger from omnigibson.utils.python_utils import Wrapper from omnigibson.objects.dataset_object import DatasetObject @@ -37,6 +38,87 @@ m.MIN_DYNAMIC_SCALE = 0.5 m.DYNAMIC_SCALE_INCREMENT = 0.1 +GOOD_MODELS = { + "jar": {"kijnrj"}, + "carton": {"causya", "msfzpz", "sxlklf"}, + "hamper": {"drgdfh", "hlgjme", "iofciz", "pdzaca", "ssfvij"}, + "hanging_plant": set(), + "hardback": {"esxakn"}, + "notebook": {"hwhisw"}, + "paperback": {"okcflv"}, + "plant_pot": {"ihnfbi", "vhglly", "ygrtaz"}, + "pot_plant": {"cvthyv", "dbjcic", "cecdwu"}, + "recycling_bin": {"nuoypc"}, + "tray": {"gsxbym", "huwhjg", "txcjux", "uekqey", "yqtlhy"}, +} + +GOOD_BBOXES = { + "basil": { + "dkuhvb": [0.07286304, 0.0545199 , 0.03108144], + }, + "basil_jar": { + "swytaw": [0.22969539, 0.19492961, 0.30791675], + }, + "bicycle_chain": { + "czrssf": [0.242, 0.012, 0.021], + }, + "clam": { + "ihhbfj": [0.078, 0.081, 0.034], + }, + "envelope": { + "urcigc": [0.004, 0.06535058, 0.10321216], + }, + "mail": { + "azunex": [0.19989018, 0.005, 0.12992871], + "gvivdi": [0.28932137, 0.005, 0.17610794], + "mbbwhn": [0.27069291, 0.005, 0.13114884], + "ojkepk": [0.19092424, 0.005, 0.13252979], + "qpwlor": [0.22472473, 0.005, 0.18983322], + }, + "pill_bottle": { + "csvdbe": [0.078, 0.078, 0.109], + "wsasmm": [0.078, 0.078, 0.109], + }, + "plant_pot": { + "ihnfbi": [0.24578613, 0.2457865 , 0.18862737], + }, + "razor": { + "jocsgp": [0.046, 0.063, 0.204], + }, + "recycling_bin": { + "nuoypc": [0.69529409, 0.80712041, 1.07168694], + }, + "tupperware": { + "mkstwr": [0.33, 0.33, 0.21], + }, +} + +BAD_MODELS = { + "bandana": {"wbhliu"}, + "curtain": {"ohvomi"}, + "cardigan": {"itrkhr"}, + "sweatshirt": {"nowqqh"}, + "jeans": {"nmvvil", "pvzxyp"}, + "pajamas": {"rcgdde"}, + "polo_shirt": {"vqbvph"}, + "vest": {"girtqm"}, # bddl NOT FIXED + "onesie": {"pbytey"}, + "dishtowel": {"ltydgg"}, + "dress": {"gtghon"}, + "hammock": {'aiftuk', 'fglfga', 'klhkgd', 'lqweda', 'qewdqa'}, + 'jacket': {'kiiium', 'nogevo', 'remcyk'}, + "quilt": {"mksdlu", "prhems"}, + "pennant": {"tfnwti"}, + "pillowcase": {"dtoahb", "yakvci"}, + "rubber_glove": {"leuiso"}, + "scarf": {"kclcrj"}, + "sock": {"vpafgj"}, + "tank_top": {"fzldgi"}, + "curtain": {"shbakk"} +} + +DO_NOT_REMESH_CLOTHS = {} +DO_NOT_REMESH_CLOTHS.update(BAD_MODELS) class UnsampleablePredicate: def _sample(self, *args, **kwargs): @@ -152,6 +234,7 @@ def process_single_condition(condition): "hot": get_unary_predicate_for_state(object_states.Heated, "hot"), "open": get_unary_predicate_for_state(object_states.Open, "open"), "toggled_on": get_unary_predicate_for_state(object_states.ToggledOn, "toggled_on"), + "on_fire": get_unary_predicate_for_state(object_states.OnFire, "on_fire"), "attached": get_binary_predicate_for_state(object_states.AttachedTo, "attached"), "overlaid": get_binary_predicate_for_state(object_states.Overlaid, "overlaid"), "folded": get_unary_predicate_for_state(object_states.Folded, "folded"), @@ -162,7 +245,7 @@ def process_single_condition(condition): "insource": ObjectStateInsourcePredicate, } -KINEMATIC_STATES_BDDL = frozenset([state.__name__.lower() for state in _KINEMATIC_STATE_SET]) +KINEMATIC_STATES_BDDL = frozenset([state.__name__.lower() for state in _KINEMATIC_STATE_SET] + ["attached"]) # BEHAVIOR-related @@ -476,6 +559,7 @@ def __init__( self._future_obj_instances = None # set of str self._inroom_object_conditions = None # list of (condition, positive) tuple self._inroom_object_scope_filtered_initial = None # dict mapping str to BDDLEntity + self._attached_objects = defaultdict(set) # dict mapping str to set of str def sample(self, validate_goal=False): """ @@ -554,6 +638,11 @@ def _prepare_scene_for_sampling(self): log.error(error_msg) return False, error_msg + error_msg = self._parse_attached_states() + if error_msg: + log.error(error_msg) + return False, error_msg + error_msg = self._build_sampling_order() if error_msg: log.error(error_msg) @@ -601,6 +690,51 @@ def _parse_inroom_object_room_assignment(self): self._inroom_object_instances.add(obj_inst) + def _parse_attached_states(self): + """ + Infers which objects are attached to which other objects. + If a category-level attachment is specified, it will be expanded to all instances of that category. + E.g. if the goal condition requires corks to be attached to bottles, every cork needs to be able to + attach to every bottle. + """ + for cond in self._activity_conditions.parsed_initial_conditions: + if cond[0] == "attached": + obj_inst, parent_inst = cond[1], cond[2] + if obj_inst not in self._object_scope or parent_inst not in self._object_scope: + return f"Object [{obj_inst}] or parent [{parent_inst}] in attached initial condition not found in object scope" + self._attached_objects[obj_inst].add(parent_inst) + + ground_attached_conditions = [] + conditions_to_check = self._activity_conditions.parsed_goal_conditions.copy() + while conditions_to_check: + new_conditions_to_check = [] + for cond in conditions_to_check: + if cond[0] == "attached": + ground_attached_conditions.append(cond) + else: + new_conditions_to_check.extend([ele for ele in cond if isinstance(ele, list)]) + conditions_to_check = new_conditions_to_check + + for cond in ground_attached_conditions: + obj_inst, parent_inst = cond[1].lstrip("?"), cond[2].lstrip("?") + if obj_inst in self._object_scope: + obj_insts = [obj_inst] + elif obj_inst in self._activity_conditions.parsed_objects: + obj_insts = self._activity_conditions.parsed_objects[obj_inst] + else: + return f"Object [{obj_inst}] in attached goal condition not found in object scope or parsed objects" + + if parent_inst in self._object_scope: + parent_insts = [parent_inst] + elif parent_inst in self._activity_conditions.parsed_objects: + parent_insts = self._activity_conditions.parsed_objects[parent_inst] + else: + return f"Parent [{parent_inst}] in attached goal condition not found in object scope or parsed objects" + + for obj_inst in obj_insts: + for parent_inst in parent_insts: + self._attached_objects[obj_inst].add(parent_inst) + def _build_sampling_order(self): """ Sampling orders is a list of lists: [[batch_1_inst_1, ... batch_1_inst_N], [batch_2_inst_1, batch_2_inst_M], ...] @@ -735,12 +869,13 @@ def _build_inroom_object_scope(self): # We allow burners to be used as if they are stoves # No need to safeguard check for subtree_substances because inroom objects will never be substances categories = OBJECT_TAXONOMY.get_subtree_categories(obj_synset) - abilities = OBJECT_TAXONOMY.get_abilities(obj_synset) # Grab all models that fully support all abilities for the corresponding category - valid_models = {cat: set(get_all_object_category_models_with_abilities(cat, abilities)) - for cat in categories} - + valid_models = {cat: set(get_all_object_category_models_with_abilities( + cat, OBJECT_TAXONOMY.get_abilities(OBJECT_TAXONOMY.get_synset_from_category(cat)))) + for cat in categories} + valid_models = {cat: (models if cat not in GOOD_MODELS else models.intersection(GOOD_MODELS[cat])) - BAD_MODELS.get(cat, set()) for cat, models in valid_models.items()} + valid_models = {cat: self._filter_model_choices_by_attached_states(models, cat, obj_inst) for cat, models in valid_models.items()} room_insts = [None] if self._scene_model is None else og.sim.scene.seg_map.room_sem_name_to_ins_name[room_type] for room_inst in room_insts: # A list of scene objects that satisfy the requested categories @@ -799,12 +934,29 @@ def _filter_object_scope(self, input_object_scope, conditions, condition_type): entity = self._object_scope[child_scope_name] conditions_to_sample.append((condition, positive, entity, child_scope_name)) - # Sort children based on their AABB so the larger objects are sampled first - conditions_to_sample = reversed(sorted(conditions_to_sample, key=lambda x: np.product(x[2].aabb_extent))) + # If we're sampling kinematics, sort children based on (a) whether they are cloth or not, and + # then (b) their AABB, so that first all rigid objects are sampled before all cloth objects, + # and within each group the larger objects are sampled first. This is needed because rigid + # objects currently don't detect collisions with cloth objects (rigid_obj.states[ContactBodies] + # is empty even when a cloth object is in contact with it). + rigid_conditions = [c for c in conditions_to_sample if c[2].prim_type != PrimType.CLOTH] + cloth_conditions = [c for c in conditions_to_sample if c[2].prim_type == PrimType.CLOTH] + conditions_to_sample = ( + list(reversed(sorted(rigid_conditions, key=lambda x: np.product(x[2].aabb_extent)))) + + list(reversed(sorted(cloth_conditions, key=lambda x: np.product(x[2].aabb_extent)))) + ) # Sample! for condition, positive, entity, child_scope_name in conditions_to_sample: - success = condition.sample(binary_state=positive) + kwargs = dict() + # Reset if we're sampling a kinematic state + if condition.STATE_NAME in {"inside", "ontop", "under"}: + kwargs["reset_before_sampling"] = True + elif condition.STATE_NAME in {"attached"}: + kwargs["bypass_alignment_checking"] = True + kwargs["check_physics_stability"] = True + kwargs["can_joint_break"] = False + success = condition.sample(binary_state=positive, **kwargs) log_msg = " ".join( [ f"{condition_type} kinematic condition sampling", @@ -885,6 +1037,69 @@ def _consolidate_room_instance(self, filtered_object_scope, condition_type): if key in room_inst_satisfied } + def _filter_model_choices_by_attached_states(self, model_choices, category, obj_inst): + # If obj_inst is a child object that depends on a parent object that has been imported or exists in the scene, + # we filter in only models that match the parent object's attachment metalinks. + if obj_inst in self._attached_objects: + parent_insts = self._attached_objects[obj_inst] + parent_objects = [] + for parent_inst in parent_insts: + # If parent_inst is not an inroom object, it must be a non-sampleable object that has already been imported. + # Grab it from the object_scope + if parent_inst not in self._inroom_object_instances: + assert self._object_scope[parent_inst] is not None + parent_objects.append([self._object_scope[parent_inst].wrapped_obj]) + # If parent_inst is an inroom object, it can refer to multiple objects in the scene in different rooms. + # We gather all of them and require that the model choice supports attachment to at least one of them. + else: + for _, parent_inst_to_parent_objs in self._inroom_object_scope.items(): + if parent_inst in parent_inst_to_parent_objs: + parent_objects.append(sum(parent_inst_to_parent_objs[parent_inst].values(), [])) + + # Help function to check if a child object can attach to a parent object + def can_attach(child_attachment_links, parent_attachment_links): + for child_link_name in child_attachment_links: + child_category = child_link_name.split("_")[1] + if child_category.endswith("F"): + continue + assert child_category.endswith("M") + parent_category = child_category[:-1] + "F" + for parent_link_name in parent_attachment_links: + if parent_category in parent_link_name: + return True + return False + + # Filter out models that don't support the attached states + new_model_choices = set() + for model_choice in model_choices: + child_attachment_links = get_attachment_metalinks(category, model_choice) + # The child model choice needs to be able to attach to all parent instances. + # For in-room parent instances, there might be multiple parent objects (e.g. different wall nails), + # and the child object needs to be able to attach to at least one of them. + if all( + any( + can_attach(child_attachment_links, get_attachment_metalinks(parent_obj.category, parent_obj.model)) + for parent_obj in parent_objs_per_inst + ) + for parent_objs_per_inst in parent_objects): + new_model_choices.add(model_choice) + + return new_model_choices + + # If obj_inst is a prent object that other objects depend on, we filter in only models that have at least some + # attachment links. + elif any(obj_inst in parents for parents in self._attached_objects.values()): + # Filter out models that don't support the attached states + new_model_choices = set() + for model_choice in model_choices: + if len(get_attachment_metalinks(category, model_choice)) > 0: + new_model_choices.add(model_choice) + return new_model_choices + + # If neither of the above cases apply, we don't need to filter the model choices + else: + return model_choices + def _import_sampleable_objects(self): """ Import all objects that can be sampled @@ -901,7 +1116,15 @@ def _import_sampleable_objects(self): num_new_obj = 0 # Only populate self.object_scope for sampleable objects available_categories = set(get_all_object_categories()) - for obj_synset in self._activity_conditions.parsed_objects: + + # Attached states introduce dependencies among objects during import time. + # For example, when importing a child object instance, we need to make sure the imported model can be attached + # to the parent object instance. We sort the object instances such that parent object instances are imported + # before child object instances. + dependencies = {key: self._attached_objects.get(key, {}) for key in self._object_instance_to_synset.keys()} + for obj_inst in list(reversed(list(nx.algorithms.topological_sort(nx.DiGraph(dependencies))))): + obj_synset = self._object_instance_to_synset[obj_inst] + # Don't populate agent if obj_synset == "agent.n.01": continue @@ -922,52 +1145,60 @@ def _import_sampleable_objects(self): return f"None of the following categories could be found in the dataset for synset {obj_synset}: " \ f"{valid_categories}" - for obj_inst in self._activity_conditions.parsed_objects[obj_synset]: - # Don't explicitly sample if future - if obj_inst in self._future_obj_instances: - self._object_scope[obj_inst] = BDDLEntity(bddl_inst=obj_inst) - continue - # Don't sample if already in room - if obj_inst in self._inroom_object_instances: - continue + # Don't explicitly sample if future + if obj_inst in self._future_obj_instances: + self._object_scope[obj_inst] = BDDLEntity(bddl_inst=obj_inst) + continue + # Don't sample if already in room + if obj_inst in self._inroom_object_instances: + continue - # Shuffle categories and sample to find a valid model - np.random.shuffle(categories) - model_choices, category = set(), None - for category in categories: - # Get all available models that support all of its synset abilities - model_choices = set(get_all_object_category_models_with_abilities( - category=category, - abilities=OBJECT_TAXONOMY.get_abilities(OBJECT_TAXONOMY.get_synset_from_category(category)), - )) - if len(model_choices) > 0: - break - - if len(model_choices) == 0: - # We failed to find ANY valid model across ALL valid categories - return f"Missing valid object models for all categories: {categories}" - - # Randomly select an object model - model = np.random.choice(list(model_choices)) - - # create the object - simulator_obj = DatasetObject( - name=f"{category}_{len(og.sim.scene.objects)}", + # Shuffle categories and sample to find a valid model + np.random.shuffle(categories) + model_choices = set() + for category in categories: + # Get all available models that support all of its synset abilities + model_choices = set(get_all_object_category_models_with_abilities( category=category, - model=model, - prim_type=PrimType.CLOTH if "cloth" in OBJECT_TAXONOMY.get_abilities(obj_synset) else PrimType.RIGID, - ) - num_new_obj += 1 + abilities=OBJECT_TAXONOMY.get_abilities(OBJECT_TAXONOMY.get_synset_from_category(category)), + )) + model_choices = model_choices if category not in GOOD_MODELS else model_choices.intersection(GOOD_MODELS[category]) + model_choices -= BAD_MODELS.get(category, set()) + model_choices = self._filter_model_choices_by_attached_states(model_choices, category, obj_inst) + if len(model_choices) > 0: + break + + if len(model_choices) == 0: + # We failed to find ANY valid model across ALL valid categories + return f"Missing valid object models for all categories: {categories}" + + # Randomly select an object model + model = np.random.choice(list(model_choices)) + + # Potentially add additional kwargs + obj_kwargs = dict() + + obj_kwargs["bounding_box"] = GOOD_BBOXES.get(category, dict()).get(model, None) + + # create the object + simulator_obj = DatasetObject( + name=f"{category}_{len(og.sim.scene.objects)}", + category=category, + model=model, + prim_type=PrimType.CLOTH if "cloth" in OBJECT_TAXONOMY.get_abilities(obj_synset) else PrimType.RIGID, + **obj_kwargs, + ) + num_new_obj += 1 - # Load the object into the simulator - assert og.sim.scene.loaded, "Scene is not loaded" - og.sim.import_object(simulator_obj) + # Load the object into the simulator + assert og.sim.scene.loaded, "Scene is not loaded" + og.sim.import_object(simulator_obj) - # Set these objects to be far-away locations - simulator_obj.set_position(np.array([100.0, 100.0, -100.0]) + np.ones(3) * num_new_obj * 5.0) + # Set these objects to be far-away locations + simulator_obj.set_position(np.array([100.0, 100.0, -100.0]) + np.ones(3) * num_new_obj * 5.0) - self._sampled_objects.add(simulator_obj) - self._object_scope[obj_inst] = BDDLEntity(bddl_inst=obj_inst, entity=simulator_obj) + self._sampled_objects.add(simulator_obj) + self._object_scope[obj_inst] = BDDLEntity(bddl_inst=obj_inst, entity=simulator_obj) og.sim.play() og.sim.stop() @@ -1039,18 +1270,34 @@ def _sample_initial_conditions_final(self): entity = self._object_scope[child_scope_name] conditions_to_sample.append((condition, positive, entity, child_scope_name)) - # If we're sampling kinematics, sort children based on their AABB, so that the larger objects - # are sampled first + # If we're sampling kinematics, sort children based on (a) whether they are cloth or not, and then + # (b) their AABB, so that first all rigid objects are sampled before cloth objects, and within each + # group the larger objects are sampled first if group == "kinematic": - conditions_to_sample = reversed(sorted(conditions_to_sample, key=lambda x: np.product(x[2].aabb_extent))) + rigid_conditions = [c for c in conditions_to_sample if c[2].prim_type != PrimType.CLOTH] + cloth_conditions = [c for c in conditions_to_sample if c[2].prim_type == PrimType.CLOTH] + conditions_to_sample = ( + list(reversed(sorted(rigid_conditions, key=lambda x: np.product(x[2].aabb_extent)))) + + list(reversed(sorted(cloth_conditions, key=lambda x: np.product(x[2].aabb_extent)))) + ) # Sample! for condition, positive, entity, child_scope_name in conditions_to_sample: success = False + + kwargs = dict() + # Reset if we're sampling a kinematic state + if condition.STATE_NAME in {"inside", "ontop", "under"}: + kwargs["reset_before_sampling"] = True + elif condition.STATE_NAME in {"attached"}: + kwargs["bypass_alignment_checking"] = True + kwargs["check_physics_stability"] = True + kwargs["can_joint_break"] = False + while True: num_trials = 1 for _ in range(num_trials): - success = condition.sample(binary_state=positive) + success = condition.sample(binary_state=positive, **kwargs) if success: # Update state state = og.sim.dump_state(serialized=False) @@ -1066,7 +1313,7 @@ def _sample_initial_conditions_final(self): # Can't re-sample non-kinematics or rescale cloth or agent, so in # those cases terminate immediately - if group != "kinematic" or "agent" in child_scope_name or entity.prim_type == PrimType.CLOTH: + if group != "kinematic" or condition.STATE_NAME == "attached" or "agent" in child_scope_name or entity.prim_type == PrimType.CLOTH: break # If any scales are equal or less than the lower threshold, terminate immediately @@ -1118,8 +1365,9 @@ def _sample_conditions(self, input_object_scope, conditions, condition_type): og.sim.stop() for obj_inst in problematic_objs: obj = self._object_scope[obj_inst] - # Can't rescale cloth or agent, so play again and then terminate immediately if found - if "agent" in obj_inst or obj.prim_type == PrimType.CLOTH: + # If the object's initial condition is attachment, or it's agent or cloth, we can't / shouldn't scale + # down, so play again and then terminate immediately + if obj_inst in self._attached_objects or "agent" in obj_inst or obj.prim_type == PrimType.CLOTH: og.sim.play() return error_msg, None assert np.all(obj.scale > m.DYNAMIC_SCALE_INCREMENT) diff --git a/omnigibson/utils/usd_utils.py b/omnigibson/utils/usd_utils.py index e3f978b5e..db1d92546 100644 --- a/omnigibson/utils/usd_utils.py +++ b/omnigibson/utils/usd_utils.py @@ -776,21 +776,28 @@ def get_mesh_volume_and_com(mesh_prim, world_frame=False): def check_extent_radius_ratio(mesh_prim): """ - Checks if the extent radius ratio of @mesh_prim is within the acceptable range for PhysX GPU acceleration (not too oblong) + Checks if the min extent in world frame and the extent radius ratio in local frame of @mesh_prim is within the + acceptable range for PhysX GPU acceleration (not too thin, and not too oblong) Ref: https://github.com/NVIDIA-Omniverse/PhysX/blob/561a0df858d7e48879cdf7eeb54cfe208f660f18/physx/source/geomutils/src/convex/GuConvexMeshData.h#L183-L190 Args: - mesh_prim (Usd.Prim): Mesh prim to check the extent radius ratio for + mesh_prim (Usd.Prim): Mesh prim to check Returns: - bool: True if the extent radius ratio is within the acceptable range, False otherwise + bool: True if the min extent (world) and the extent radius ratio (local frame) is acceptable, False otherwise """ mesh_type = mesh_prim.GetPrimTypeInfo().GetTypeName() # Non-mesh prims are always considered to be within the acceptable range if mesh_type != "Mesh": return True + trimesh_mesh_world = mesh_prim_to_trimesh_mesh(mesh_prim, include_normals=False, include_texcoord=False, world_frame=True) + min_extent = trimesh_mesh_world.extents.min() + # If the mesh is too flat in the world frame, omniverse cannot create convex mesh for it + if min_extent < 1e-5: + return False + trimesh_mesh = mesh_prim_to_trimesh_mesh(mesh_prim, include_normals=False, include_texcoord=False, world_frame=False) if not trimesh_mesh.is_volume: trimesh_mesh = trimesh_mesh.convex_hull From 28e40066425c358f148e6f298d6f8a08bb39e376 Mon Sep 17 00:00:00 2001 From: cremebrule <84cremebrule@gmail.com> Date: Sun, 17 Mar 2024 01:04:20 -0700 Subject: [PATCH 2/2] clarify bad models to be cloth, raise error if bad cloth model is imported, do not use bounding box model blacklist --- omnigibson/objects/dataset_object.py | 12 ++++++++---- omnigibson/scenes/scene_base.py | 9 --------- omnigibson/utils/bddl_utils.py | 8 +++----- 3 files changed, 11 insertions(+), 18 deletions(-) diff --git a/omnigibson/objects/dataset_object.py b/omnigibson/objects/dataset_object.py index b5c59d124..2136d7634 100644 --- a/omnigibson/objects/dataset_object.py +++ b/omnigibson/objects/dataset_object.py @@ -104,15 +104,19 @@ def __init__( load_config = dict() if load_config is None else load_config load_config["bounding_box"] = bounding_box - # TODO: Remove once meshes are fixed - from omnigibson.utils.bddl_utils import DO_NOT_REMESH_CLOTHS - load_config["remesh"] = model not in DO_NOT_REMESH_CLOTHS.get(category, set()) - # Infer the correct usd path to use if model is None: available_models = get_all_object_category_models(category=category) assert len(available_models) > 0, f"No available models found for category {category}!" model = np.random.choice(available_models) + + # If the model is in BAD_CLOTH_MODELS, raise an error for now -- this is a model that's unstable and needs to be fixed + # TODO: Remove this once the asset is fixed! + from omnigibson.utils.bddl_utils import BAD_CLOTH_MODELS + if prim_type == PrimType.CLOTH and model in BAD_CLOTH_MODELS.get(category, dict()): + raise ValueError(f"Cannot create cloth object category: {category}, model: {model} because it is " + f"currently broken ): This will be fixed in the next release!") + self._model = model usd_path = self.get_usd_path(category=category, model=model) diff --git a/omnigibson/scenes/scene_base.py b/omnigibson/scenes/scene_base.py index 04ceb3f03..4c5faec90 100644 --- a/omnigibson/scenes/scene_base.py +++ b/omnigibson/scenes/scene_base.py @@ -32,9 +32,6 @@ # Global dicts that will contain mappings REGISTERED_SCENES = dict() -BOUNDING_CUBE_OBJECTS = { - "xbfgjc": {"base_link"}, -} class Scene(Serializable, Registerable, Recreatable, ABC): """ @@ -240,12 +237,6 @@ def _load_objects_from_scene_file(self): obj = create_object_from_init_info(obj_info) # Import into the simulator og.sim.import_object(obj) - if isinstance(obj, DatasetObject) and obj.model in BOUNDING_CUBE_OBJECTS: - link_names = BOUNDING_CUBE_OBJECTS[obj.model] - for link_name in link_names: - link = obj.links[link_name] - for col_mesh in link.collision_meshes.values(): - col_mesh.set_collision_approximation("boundingCube") # Set the init pose accordingly obj.set_position_orientation( position=init_state[obj_name]["root_link"]["pos"], diff --git a/omnigibson/utils/bddl_utils.py b/omnigibson/utils/bddl_utils.py index 31f61570b..0356e3c99 100644 --- a/omnigibson/utils/bddl_utils.py +++ b/omnigibson/utils/bddl_utils.py @@ -93,7 +93,7 @@ }, } -BAD_MODELS = { +BAD_CLOTH_MODELS = { "bandana": {"wbhliu"}, "curtain": {"ohvomi"}, "cardigan": {"itrkhr"}, @@ -117,8 +117,6 @@ "curtain": {"shbakk"} } -DO_NOT_REMESH_CLOTHS = {} -DO_NOT_REMESH_CLOTHS.update(BAD_MODELS) class UnsampleablePredicate: def _sample(self, *args, **kwargs): @@ -874,7 +872,7 @@ def _build_inroom_object_scope(self): valid_models = {cat: set(get_all_object_category_models_with_abilities( cat, OBJECT_TAXONOMY.get_abilities(OBJECT_TAXONOMY.get_synset_from_category(cat)))) for cat in categories} - valid_models = {cat: (models if cat not in GOOD_MODELS else models.intersection(GOOD_MODELS[cat])) - BAD_MODELS.get(cat, set()) for cat, models in valid_models.items()} + valid_models = {cat: (models if cat not in GOOD_MODELS else models.intersection(GOOD_MODELS[cat])) - BAD_CLOTH_MODELS.get(cat, set()) for cat, models in valid_models.items()} valid_models = {cat: self._filter_model_choices_by_attached_states(models, cat, obj_inst) for cat, models in valid_models.items()} room_insts = [None] if self._scene_model is None else og.sim.scene.seg_map.room_sem_name_to_ins_name[room_type] for room_inst in room_insts: @@ -1163,7 +1161,7 @@ def _import_sampleable_objects(self): abilities=OBJECT_TAXONOMY.get_abilities(OBJECT_TAXONOMY.get_synset_from_category(category)), )) model_choices = model_choices if category not in GOOD_MODELS else model_choices.intersection(GOOD_MODELS[category]) - model_choices -= BAD_MODELS.get(category, set()) + model_choices -= BAD_CLOTH_MODELS.get(category, set()) model_choices = self._filter_model_choices_by_attached_states(model_choices, category, obj_inst) if len(model_choices) > 0: break